├── .cirrus.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── codecov.yml ├── dependabot.yml ├── problem-matchers │ └── rust.json └── workflows │ ├── ci.yaml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYRIGHT ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── SECURITY.md ├── bin ├── su.rs ├── sudo.rs └── visudo.rs ├── build.rs ├── clippy.toml ├── docs ├── audit │ └── audit-report-sudo-rs.pdf ├── man │ ├── su.1.man │ ├── su.1.md │ ├── sudo.8.man │ ├── sudo.8.md │ ├── sudoers.5.man │ ├── sudoers.5.md │ ├── visudo.8.man │ └── visudo.8.md ├── sudo-cve.md └── undocumented-ogsudo-behavior.md ├── get-pam-variant.bash ├── make-lcov-info.bash ├── proofs └── sudoers.mlw ├── src ├── apparmor │ ├── mod.rs │ └── sys.rs ├── common │ ├── bin_serde.rs │ ├── command.rs │ ├── context.rs │ ├── error.rs │ ├── mod.rs │ ├── path.rs │ ├── resolve.rs │ └── string.rs ├── cutils │ └── mod.rs ├── defaults │ ├── mod.rs │ └── settings_dsl.rs ├── exec │ ├── event.rs │ ├── io_util.rs │ ├── mod.rs │ ├── no_pty.rs │ ├── noexec.rs │ └── use_pty │ │ ├── backchannel.rs │ │ ├── mod.rs │ │ ├── monitor.rs │ │ ├── parent.rs │ │ └── pipe │ │ ├── mod.rs │ │ └── ring_buffer.rs ├── lib.rs ├── log │ ├── mod.rs │ ├── simple_logger.rs │ └── syslog.rs ├── macros.rs ├── pam │ ├── converse.rs │ ├── error.rs │ ├── mod.rs │ ├── rpassword.rs │ ├── securemem.rs │ ├── sys_linuxpam.rs │ ├── sys_openpam.rs │ └── wrapper.h ├── su │ ├── cli.rs │ ├── context.rs │ ├── help.rs │ └── mod.rs ├── sudo │ ├── cli │ │ ├── help.rs │ │ ├── mod.rs │ │ └── tests.rs │ ├── diagnostic.rs │ ├── env │ │ ├── environment.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── wildcard_match.rs │ ├── mod.rs │ ├── pam.rs │ ├── pipeline.rs │ └── pipeline │ │ └── list.rs ├── sudoers │ ├── ast.rs │ ├── ast_names.rs │ ├── basic_parser.rs │ ├── char_stream.rs │ ├── entry.rs │ ├── entry │ │ └── verbose.rs │ ├── mod.rs │ ├── policy.rs │ ├── test │ │ └── mod.rs │ └── tokens.rs ├── system │ ├── audit.rs │ ├── file │ │ ├── chown.rs │ │ ├── lock.rs │ │ ├── mod.rs │ │ └── tmpdir.rs │ ├── interface.rs │ ├── mod.rs │ ├── signal │ │ ├── handler.rs │ │ ├── info.rs │ │ ├── mod.rs │ │ ├── set.rs │ │ └── stream.rs │ ├── term │ │ ├── mod.rs │ │ └── user_term.rs │ ├── time.rs │ ├── timestamp.rs │ └── wait.rs └── visudo │ ├── cli.rs │ ├── help.rs │ └── mod.rs ├── test-framework ├── .gitignore ├── Cargo.toml ├── README.md ├── e2e-tests │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── pty.rs │ │ ├── regression.rs │ │ ├── su.rs │ │ └── su │ │ ├── flag_pty.rs │ │ └── signal_handling │ │ ├── expects-signal.sh │ │ ├── kill-su-parent.sh │ │ ├── kill-su.sh │ │ ├── mod.rs │ │ └── sigtstp.bash ├── sudo-compliance-tests │ ├── Cargo.toml │ └── src │ │ ├── helpers.rs │ │ ├── lib.rs │ │ ├── macros.rs │ │ ├── snapshots │ │ ├── flag_group │ │ │ ├── unassigned_group_id_is_rejected-2.snap │ │ │ └── unassigned_group_id_is_rejected.snap │ │ ├── flag_login │ │ │ ├── if_home_directory_does_not_exist_executes_program_without_changing_the_working_directory-2.snap │ │ │ ├── if_home_directory_does_not_exist_executes_program_without_changing_the_working_directory.snap │ │ │ ├── insufficient_permissions_to_execute_shell.snap │ │ │ └── shell_does_not_exist.snap │ │ ├── flag_shell │ │ │ ├── shell_does_not_exist.snap │ │ │ └── shell_is_not_executable.snap │ │ ├── flag_user │ │ │ ├── unassigned_user_id_is_rejected-2.snap │ │ │ └── unassigned_user_id_is_rejected.snap │ │ ├── misc │ │ │ └── user_not_in_passwd_database_cannot_use_sudo.snap │ │ ├── passwd │ │ │ └── explicit_passwd_overrides_nopasswd.snap │ │ ├── path_search │ │ │ └── when_path_is_unset_does_not_search_in_default_path_set_for_command_execution.snap │ │ ├── secure_path │ │ │ └── dash_dash_before_flag_is_an_error.snap │ │ ├── sudoers │ │ │ ├── cmnd │ │ │ │ ├── command_specified_not_by_absolute_path_is_rejected.snap │ │ │ │ └── given_specific_command_then_other_command_is_not_allowed.snap │ │ │ ├── cmnd_alias │ │ │ │ ├── another_negation_combination.snap │ │ │ │ ├── combined_cmnd_aliases.snap │ │ │ │ ├── command_alias_negation.snap │ │ │ │ ├── command_specified_not_by_absolute_path_is_rejected.snap │ │ │ │ ├── negation_not_order_sensitive.snap │ │ │ │ ├── one_more_negation_combination.snap │ │ │ │ ├── runas_override-2.snap │ │ │ │ ├── runas_override.snap │ │ │ │ ├── tripple_negation_combination-2.snap │ │ │ │ ├── tripple_negation_combination.snap │ │ │ │ └── unlisted_cmnd_fails.snap │ │ │ ├── host_alias │ │ │ │ ├── combined_host_aliases.snap │ │ │ │ ├── host_alias_negation.snap │ │ │ │ ├── negation_not_order_sensitive.snap │ │ │ │ └── unlisted_host_fails.snap │ │ │ ├── host_list │ │ │ │ ├── given_specific_hostname_then_sudo_from_different_hostname_is_rejected.snap │ │ │ │ └── negation_rejects.snap │ │ │ ├── run_as │ │ │ │ ├── supplemental_group_matching.snap │ │ │ │ ├── when_empty_then_as_someone_else_is_not_allowed.snap │ │ │ │ ├── when_only_group_is_specified_then_as_some_user_is_not_allowed-2.snap │ │ │ │ ├── when_only_group_is_specified_then_as_some_user_is_not_allowed.snap │ │ │ │ ├── when_specific_group_then_as_a_different_group_is_not_allowed-2.snap │ │ │ │ ├── when_specific_group_then_as_a_different_group_is_not_allowed.snap │ │ │ │ ├── when_specific_user_then_as_a_different_user_is_not_allowed.snap │ │ │ │ └── when_specific_user_then_as_self_is_not_allowed.snap │ │ │ ├── runas_alias │ │ │ │ ├── negation_on_user.snap │ │ │ │ ├── runas_alias_negation.snap │ │ │ │ ├── when_only_groupname_is_given_user_arg_fails.snap │ │ │ │ ├── when_only_username_is_given_group_arg_fails.snap │ │ │ │ └── when_specific_user_then_as_a_different_user_is_not_allowed.snap │ │ │ ├── secure_path │ │ │ │ └── if_set_it_does_not_search_in_original_user_path.snap │ │ │ └── user_list │ │ │ │ ├── negated_subgroup.snap │ │ │ │ ├── negated_supergroup-2.snap │ │ │ │ ├── negated_supergroup.snap │ │ │ │ ├── negation_excludes_group_members.snap │ │ │ │ ├── no_match.snap │ │ │ │ └── user_alias_works.snap │ │ └── visudo │ │ │ ├── flag_file │ │ │ └── passes_temporary_file_to_editor.snap │ │ │ ├── passes_temporary_file_to_editor.snap │ │ │ ├── stderr_message_when_file_is_not_modified.snap │ │ │ └── temporary_file_is_deleted_during_edition.snap │ │ ├── su.rs │ │ ├── su │ │ ├── cli.rs │ │ ├── env.rs │ │ ├── flag_command.rs │ │ ├── flag_group.rs │ │ ├── flag_login.rs │ │ ├── flag_preserve_environment.rs │ │ ├── flag_shell.rs │ │ ├── flag_supp_group.rs │ │ ├── flag_whitelist_environment.rs │ │ ├── limits.rs │ │ ├── pam.rs │ │ └── syslog.rs │ │ ├── sudo.rs │ │ ├── sudo │ │ ├── apparmor.rs │ │ ├── child_process.rs │ │ ├── child_process │ │ │ └── signal_handling │ │ │ │ ├── change-size.sh │ │ │ │ ├── expects-signal.sh │ │ │ │ ├── kill-sudo-parent.sh │ │ │ │ ├── kill-sudo.sh │ │ │ │ ├── mod.rs │ │ │ │ ├── print-sizes.sh │ │ │ │ └── sigtstp.bash │ │ ├── cli.rs │ │ ├── env_reset.rs │ │ ├── flag_chdir.rs │ │ ├── flag_group.rs │ │ ├── flag_help.rs │ │ ├── flag_list.rs │ │ ├── flag_list │ │ │ ├── credential_caching.rs │ │ │ ├── flag_other_user.rs │ │ │ ├── long_format │ │ │ │ ├── mod.rs │ │ │ │ └── snapshots │ │ │ │ │ ├── command_alias.snap │ │ │ │ │ ├── command_arguments.snap │ │ │ │ │ ├── complex_runas.snap │ │ │ │ │ ├── cwd_across_runas_groups.snap │ │ │ │ │ ├── cwd_any.snap │ │ │ │ │ ├── cwd_multiple_commands.snap │ │ │ │ │ ├── cwd_multiple_runas_groups.snap │ │ │ │ │ ├── cwd_nopasswd.snap │ │ │ │ │ ├── cwd_not_in_first_position.snap │ │ │ │ │ ├── cwd_override.snap │ │ │ │ │ ├── cwd_override_across_runas_groups.snap │ │ │ │ │ ├── cwd_path.snap │ │ │ │ │ ├── empty_runas.snap │ │ │ │ │ ├── group_runas.snap │ │ │ │ │ ├── implicit_runas_group.snap │ │ │ │ │ ├── multiple_commands.snap │ │ │ │ │ ├── multiple_group_runas.snap │ │ │ │ │ ├── multiple_lines.snap │ │ │ │ │ ├── multiple_runas_groups.snap │ │ │ │ │ ├── multiple_users_runas.snap │ │ │ │ │ ├── negated_command_alias.snap │ │ │ │ │ ├── no_runas.snap │ │ │ │ │ ├── nopasswd.snap │ │ │ │ │ ├── nopasswd_across_runas_groups.snap │ │ │ │ │ ├── nopasswd_passwd_on_same_command.snap │ │ │ │ │ ├── nopasswd_passwd_override.snap │ │ │ │ │ ├── nopasswd_passwd_override_across_runas_groups.snap │ │ │ │ │ ├── not_group_runas.snap │ │ │ │ │ ├── not_user_runas.snap │ │ │ │ │ ├── passwd.snap │ │ │ │ │ ├── passwd_across_runas_groups.snap │ │ │ │ │ ├── passwd_nopasswd_override.snap │ │ │ │ │ ├── user_group_id_runas.snap │ │ │ │ │ ├── user_group_runas.snap │ │ │ │ │ ├── user_id_runas.snap │ │ │ │ │ ├── user_non_unix_group_id_runas.snap │ │ │ │ │ ├── user_non_unix_group_runas.snap │ │ │ │ │ └── user_runas.snap │ │ │ ├── needs_auth.rs │ │ │ ├── nopasswd.rs │ │ │ ├── not_allowed.rs │ │ │ ├── short_format │ │ │ │ ├── mod.rs │ │ │ │ └── snapshots │ │ │ │ │ ├── command_alias.snap │ │ │ │ │ ├── command_arguments.snap │ │ │ │ │ ├── complex_runas.snap │ │ │ │ │ ├── cwd_across_runas_groups.snap │ │ │ │ │ ├── cwd_any.snap │ │ │ │ │ ├── cwd_multiple_commands.snap │ │ │ │ │ ├── cwd_multiple_runas_groups.snap │ │ │ │ │ ├── cwd_nopasswd.snap │ │ │ │ │ ├── cwd_not_in_first_position.snap │ │ │ │ │ ├── cwd_override.snap │ │ │ │ │ ├── cwd_override_across_runas_groups.snap │ │ │ │ │ ├── cwd_path.snap │ │ │ │ │ ├── empty_runas.snap │ │ │ │ │ ├── empty_runas_with_colon.snap │ │ │ │ │ ├── group_runas.snap │ │ │ │ │ ├── implicit_runas_group.snap │ │ │ │ │ ├── multiple_commands.snap │ │ │ │ │ ├── multiple_group_runas.snap │ │ │ │ │ ├── multiple_lines.snap │ │ │ │ │ ├── multiple_runas_groups.snap │ │ │ │ │ ├── multiple_users_runas.snap │ │ │ │ │ ├── negated_command_alias.snap │ │ │ │ │ ├── no_runas.snap │ │ │ │ │ ├── nopasswd.snap │ │ │ │ │ ├── nopasswd_across_runas_groups.snap │ │ │ │ │ ├── nopasswd_passwd_on_same_command.snap │ │ │ │ │ ├── nopasswd_passwd_override.snap │ │ │ │ │ ├── nopasswd_passwd_override_across_runas_groups.snap │ │ │ │ │ ├── not_group_runas.snap │ │ │ │ │ ├── not_user_runas.snap │ │ │ │ │ ├── passwd.snap │ │ │ │ │ ├── passwd_across_runas_groups.snap │ │ │ │ │ ├── passwd_nopasswd_override.snap │ │ │ │ │ ├── user_group_id_runas.snap │ │ │ │ │ ├── user_group_runas.snap │ │ │ │ │ ├── user_id_runas.snap │ │ │ │ │ ├── user_non_unix_group_id_runas.snap │ │ │ │ │ ├── user_non_unix_group_runas.snap │ │ │ │ │ └── user_runas.snap │ │ │ └── sudoers_list.rs │ │ ├── flag_login.rs │ │ ├── flag_non_interactive.rs │ │ ├── flag_prompt.rs │ │ ├── flag_shell.rs │ │ ├── flag_user.rs │ │ ├── flag_version.rs │ │ ├── lecture.rs │ │ ├── lecture_file.rs │ │ ├── limits.rs │ │ ├── misc.rs │ │ ├── misc │ │ │ └── read-parents-open-file-descriptor.bash │ │ ├── nopasswd.rs │ │ ├── pam.rs │ │ ├── pam │ │ │ └── env.rs │ │ ├── pass_auth.rs │ │ ├── pass_auth │ │ │ ├── stdin.rs │ │ │ └── tty.rs │ │ ├── passwd.rs │ │ ├── password_retry.rs │ │ ├── password_retry │ │ │ └── time-password-retry.sh │ │ ├── path_search.rs │ │ ├── perms.rs │ │ ├── sudo_ps1.rs │ │ ├── sudoers.rs │ │ ├── sudoers │ │ │ ├── cmnd.rs │ │ │ ├── cmnd_alias.rs │ │ │ ├── cwd.rs │ │ │ ├── env.rs │ │ │ ├── env │ │ │ │ ├── check.rs │ │ │ │ └── keep.rs │ │ │ ├── host_alias.rs │ │ │ ├── host_list.rs │ │ │ ├── include.rs │ │ │ ├── includedir.rs │ │ │ ├── noexec.rs │ │ │ ├── run_as.rs │ │ │ ├── runas_alias.rs │ │ │ ├── secure_path.rs │ │ │ ├── specific_defaults.rs │ │ │ ├── timestamp_timeout.rs │ │ │ └── user_list.rs │ │ ├── syslog.rs │ │ ├── timestamp.rs │ │ ├── timestamp │ │ │ ├── remove.rs │ │ │ ├── reset.rs │ │ │ └── validate.rs │ │ └── use_pty.rs │ │ ├── visudo.rs │ │ └── visudo │ │ ├── flag_check.rs │ │ ├── flag_file.rs │ │ ├── flag_help.rs │ │ ├── flag_no_includes.rs │ │ ├── flag_owner.rs │ │ ├── flag_perms.rs │ │ ├── flag_quiet.rs │ │ ├── flag_strict.rs │ │ ├── flag_version.rs │ │ ├── include.rs │ │ ├── kill-visudo.sh │ │ ├── sudoers.rs │ │ ├── sudoers │ │ ├── editor.rs │ │ └── env_editor.rs │ │ └── what_now_prompt.rs └── sudo-test │ ├── Cargo.toml │ └── src │ ├── constants.rs │ ├── docker.rs │ ├── docker │ └── command.rs │ ├── helpers.rs │ ├── lib.rs │ ├── ours.freebsd.Dockerfile │ ├── ours.freebsd.Dockerfile.dockerignore │ ├── ours.linux.Dockerfile │ ├── ours.linux.Dockerfile.dockerignore │ ├── theirs.freebsd.Dockerfile │ └── theirs.linux.Dockerfile └── util ├── Dockerfile-release ├── build-release.sh ├── generate-docs.sh ├── pandoc.sh └── update-version.sh /.cirrus.yml: -------------------------------------------------------------------------------- 1 | task: 2 | name: Cirrus CI / freebsd unit tests 3 | freebsd_instance: 4 | image_family: freebsd-14-2 5 | memory: 2GB 6 | setup_rust_script: 7 | - pkg install -y git-tiny 8 | - curl https://sh.rustup.rs -sSf --output rustup.sh 9 | - sh rustup.sh -y --profile=minimal 10 | test_script: 11 | - . $HOME/.cargo/env 12 | # We skip a couple of tests which fail when running as root. 13 | - cargo test --workspace --all-targets --release -- --skip group_as_non_root --skip test_secure_open_cookie_file 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. Please check the issue tracker for similar issues before opening a new one. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Compile 'sudo-rs' '....' 16 | 2. Write the following contents to `/etc/sudoers-rs` '....' 17 | 3. Run the following command '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Environment (please complete the following information):** 24 | - Linux distribution: [e.g. Ubuntu 23.04] 25 | - `sudo-rs` commit hash: [e.g. `d085c0a`] 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the feature you'd like see implemented in `sudo-rs`** 11 | A clear and concise description of what you want to happen. 12 | 13 | **What problem can be solved with this feature?** 14 | A clear and concise description of what the problem is and how this feature would solve such feature. Ex. I'm always frustrated when [...] and this feature would fix this problem because [...] 15 | 16 | **Describe alternatives you've considered** 17 | Are you sure this problem cannot be solved by an already existing feature? You could also add any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Describe the changes done on this pull request** 2 | A clear and concise description of the changes done by your pull request. 3 | 4 | **Pull Request Checklist** 5 | - [] I have read and accepted the [code of conduct](https://github.com/trifectatechfoundation/sudo-rs/blob/master/CODE_OF_CONDUCT.md) for this project. 6 | - [] I have tested, formatted and ran clippy over my changes. 7 | - [] I have commented and documented my changes. 8 | - [] This pull request will fix issue https://github.com/trifectatechfoundation/sudo-rs/issues/<#issue> where a proper discussion about a solution has taken place. 9 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "test-binaries" 3 | - "test-framework" 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | informational: true 9 | patch: 10 | default: 11 | informational: true 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/problem-matchers/rust.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "rust", 5 | "pattern": [ 6 | { 7 | "regexp": "^(warning|warn|error)(\\[(.*)\\])?: (.*)$", 8 | "severity": 1, 9 | "message": 4, 10 | "code": 3 11 | }, 12 | { 13 | "regexp": "^([\\s->=]*(.*):(\\d*):(\\d*)|.*)$", 14 | "file": 2, 15 | "line": 3, 16 | "column": 4 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /build/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # Code coverage in lcov format 10 | /lcov.info 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We abide by the [Rust Code of Conduct][coc] and ask that you do as well. 4 | 5 | [coc]: https://www.rust-lang.org/policies/code-of-conduct 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to sudo-rs 2 | 3 | We welcome contributions to building a memory safe sudo / su implementation; this document lists 4 | some ways in which you can help. 5 | 6 | This project is about building a "drop-in replacement" for sudo and su. That does not mean we want 7 | to copy *all* of the behavior of original sudo or other su implementations. 8 | 9 | Whenever we add a feature, sudo becomes more complex, and unforeseen interactions due to complexity 10 | can result in security issues. Also, sudo has some features for backwards compatibility only---it makes no 11 | sense for us to re-implement a feature that by its nature won't be very well-used in practice. Other features 12 | have a very specific use-case in mind (for example, matching command lines with [regular expressions](https://xkcd.com/1171/)), 13 | which are very complex to use and would require the inclusion of a third-party library. 14 | 15 | I.e. every time we add a feature, we have to weigh its benefits to the cost of adding the feature. 16 | For this, the sudo-rs Core Team has adopted a few criteria for inclusions of features in sudo / su: 17 | 18 | 1. Security is more important than functionality. 19 | 2. Simplicity is preferred over complexity. 20 | 3. A feature to be added should actually *solve* a problem. 21 | 4. Features must support a common and reasonable use case. 22 | 5. Dependencies must be kept to an absolute minimum. 23 | 24 | ## Feature requests 25 | 26 | The easiest way to contribute is to request a feature that we currently do not have; use 27 | the issue tracker for this and explain the situation. Things that are currently possible 28 | with original sudo and that pass the above-mentioned criteria are likely to be accepted. 29 | 30 | ## Testing sudo 31 | 32 | You can install and run sudo on your personal system, or any other non-mission critical 33 | machine. We recommend installing it in `/usr/local/bin` so you have original sudo as a backup. 34 | 35 | Although sudo-rs is thoroughly tested for every change we make, a real-world test like this 36 | can uncover subtle problems in technical parts, or uncover common sudo use cases that we 37 | ignored so far. 38 | 39 | ## Small contributions 40 | 41 | You can also go through our code --- if you see any small mistakes or have suggestions 42 | please create an issue for them. If it is really a minor issue, like a typo or formatting 43 | issue, you can immediately create a pull request. 44 | 45 | ## Security auditing 46 | 47 | One way you can help is by looking at the security of our code and proposing fixes in it. 48 | More eyeballs spot more problems. 49 | 50 | If you find a security problem that can be used to used to compromise a system, 51 | do follow our [security policy] and report a vulnerability instead of using the 52 | issue tracker. 53 | 54 | [security policy]: https://github.com/trifectatechfoundation/sudo-rs/security/policy 55 | 56 | ## Working on a bigger issue 57 | 58 | If you want to pick up an issue in the issue tracker, please reach out to the 59 | sudo-rs team first. The easiest way to do this is to comment on the issue. If you want 60 | to work on something that is not on the issue tracker, do make an issue *before* you 61 | begin to make sure your work will not be conflicting with ours. 62 | 63 | 64 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-2025 Trifecta Tech Foundation and contributors 2 | Copyright (c) 1994-1996, 1998-2024 Todd C. Miller 3 | 4 | Except as otherwise noted (below and/or in individual files), sudo-rs is 5 | licensed under the Apache License, Version 2.0 or 6 | or the MIT license 7 | or , at your option. 8 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "diff" 7 | version = "0.1.13" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 10 | 11 | [[package]] 12 | name = "glob" 13 | version = "0.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 16 | 17 | [[package]] 18 | name = "libc" 19 | version = "0.2.172" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 22 | 23 | [[package]] 24 | name = "log" 25 | version = "0.4.27" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 28 | 29 | [[package]] 30 | name = "pretty_assertions" 31 | version = "1.4.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 34 | dependencies = [ 35 | "diff", 36 | "yansi", 37 | ] 38 | 39 | [[package]] 40 | name = "sudo-rs" 41 | version = "0.2.6" 42 | dependencies = [ 43 | "glob", 44 | "libc", 45 | "log", 46 | "pretty_assertions", 47 | ] 48 | 49 | [[package]] 50 | name = "yansi" 51 | version = "1.0.1" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 54 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sudo-rs" 3 | description = "A memory safe implementation of sudo and su." 4 | version = "0.2.6" 5 | license = "Apache-2.0 OR MIT" 6 | edition = "2021" 7 | repository = "https://github.com/trifectatechfoundation/sudo-rs" 8 | homepage = "https://github.com/trifectatechfoundation/sudo-rs" 9 | publish = true 10 | categories = ["command-line-interface"] 11 | 12 | rust-version = "1.70" 13 | 14 | default-run = "sudo" 15 | 16 | [lib] 17 | path = "src/lib.rs" 18 | 19 | [[bin]] 20 | name = "sudo" 21 | path = "bin/sudo.rs" 22 | 23 | [[bin]] 24 | name = "su" 25 | path = "bin/su.rs" 26 | 27 | [[bin]] 28 | name = "visudo" 29 | path = "bin/visudo.rs" 30 | 31 | [dependencies] 32 | libc = "0.2.152" 33 | glob = "0.3.0" 34 | log = { version = "0.4.11", features = ["std"] } 35 | 36 | [dev-dependencies] 37 | pretty_assertions = "1.2.1" 38 | 39 | [features] 40 | default = [] 41 | 42 | # when enabled, use "sudo-i" PAM service name for sudo -i instead of "sudo" 43 | # ONLY ENABLE THIS FEATURE if you know that original sudo uses "sudo-i" 44 | # on the system you are building sudo for (e.g. Debian, Fedora, but not Arch) 45 | pam-login = [] 46 | 47 | # this enables enforcing of AppArmor profiles 48 | apparmor = [] 49 | 50 | # enable detailed logging (use for development only) to /tmp 51 | # this will compromise the security of sudo-rs somewhat 52 | dev = [] 53 | 54 | # this feature should never be enabled, but it is here to prevent accidental 55 | # compilation using "cargo --all-features", which should not be used on sudo-rs 56 | do-not-use-all-features = [] 57 | 58 | [profile.release] 59 | strip = "symbols" 60 | lto = true 61 | opt-level = "s" 62 | 63 | [lints.rust] 64 | unsafe_op_in_unsafe_fn = { level = "deny" } 65 | 66 | [lints.clippy] 67 | undocumented_unsafe_blocks = "warn" 68 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-2025 Trifecta Tech Foundation 2 | and contributors 3 | Copyright (c) 1994-1996, 1998-2024 Todd C. Miller 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PAM_SRC_DIR = src/pam 2 | 3 | BINDGEN_CMD = bindgen --allowlist-function '^pam_.*$$' --allowlist-var '^PAM_.*$$' --opaque-type pam_handle_t --blocklist-function pam_vsyslog --blocklist-function pam_vprompt --blocklist-function pam_vinfo --blocklist-function pam_verror --blocklist-type '.*va_list.*' --ctypes-prefix libc --no-layout-tests --sort-semantically 4 | 5 | PAM_VARIANT = $$(./get-pam-variant.bash) 6 | 7 | .PHONY: all clean pam-sys pam-sys-diff 8 | 9 | pam-sys-diff: 10 | @$(BINDGEN_CMD) $(PAM_SRC_DIR)/wrapper.h | \ 11 | sed 's/rust-bindgen [0-9]*\.[0-9]*\.[0-9]*/&, minified by cargo-minify/' | \ 12 | diff --color=auto $(PAM_SRC_DIR)/sys_$(PAM_VARIANT).rs - \ 13 | || (echo run \'make -B pam-sys\' to apply these changes && false) 14 | @echo $(PAM_SRC_DIR)/sys_$(PAM_VARIANT).rs does not need to be re-generated 15 | 16 | # use 'make pam-sys' to re-generate the sys.rs file for your local platform 17 | pam-sys: 18 | $(BINDGEN_CMD) $(PAM_SRC_DIR)/wrapper.h --output $(PAM_SRC_DIR)/sys_$(PAM_VARIANT).rs 19 | cargo minify --apply --allow-dirty 20 | sed -i.bak 's/rust-bindgen [0-9]*\.[0-9]*\.[0-9]*/&, minified by cargo-minify/' $(PAM_SRC_DIR)/sys_$(PAM_VARIANT).rs 21 | rm $(PAM_SRC_DIR)/sys_$(PAM_VARIANT).rs.bak 22 | 23 | clean: 24 | rm $(PAM_SRC_DIR)/sys.rs 25 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security policy 2 | **Do not report security vulnerabilities through public GitHub issues.** 3 | 4 | Instead, you can report them using [our security page](https://github.com/trifectatechfoundation/sudo-rs/security). Alternatively, you can also send them 5 | by email to security+sudo@tweedegolf.com. You can encrypt your email using GnuPG if you want. Use the GPG key with fingerprint 6 | [C2E4 CAC4 B122 25DE 1C3B B1C9 289D 0820 03D0 1E95](https://keys.openpgp.org/search?q=C2E4CAC4B12225DE1C3BB1C9289D082003D01E95). 7 | 8 | Include as much of the following information: 9 | 10 | * Type of issue (e.g. buffer overflow, privilege escalation, etc). 11 | * The location of the affected source code (tag/branch/commit or direct URL). 12 | * Any special configuration required to reproduce the issue. 13 | * The Linux distribution affected. 14 | * Step-by-step instructions to reproduce the issue. 15 | * Impact of the issue, including how an attacker might exploit the issue. 16 | 17 | If you have found a bug that also exists in the original sudo (which, although unlikely, means it is a very serious issue), you **must** 18 | also follow the steps at https://www.sudo.ws/security/policy/ 19 | 20 | ## Preferred Languages 21 | We prefer to receive reports in English. If necessary, we also understand Spanish, German and Dutch. 22 | 23 | ## Disclosure Policy 24 | Like original sudo, we adhere to the principle of [Coordinated Vulnerability Disclosure](https://vuls.cert.org/confluence/display/CVD/Executive+Summary). 25 | 26 | # Security Advisories 27 | Security advisories will be published [on GitHub](https://github.com/trifectatechfoundation/sudo-rs/security/advisories) and possibly through other channels. 28 | -------------------------------------------------------------------------------- /bin/su.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | sudo_rs::su_main(); 3 | } 4 | -------------------------------------------------------------------------------- /bin/sudo.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | sudo_rs::sudo_main(); 3 | } 4 | -------------------------------------------------------------------------------- /bin/visudo.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | sudo_rs::visudo_main() 3 | } 4 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | // Return the first existing path given a list of paths as string slices 4 | fn get_first_path(paths: &[&'static str]) -> Option<&'static str> { 5 | paths.iter().find(|p| Path::new(p).exists()).copied() 6 | } 7 | 8 | fn main() { 9 | let path_zoneinfo: &str = get_first_path(&[ 10 | "/usr/share/zoneinfo", 11 | "/usr/share/lib/zoneinfo", 12 | "/usr/lib/zoneinfo", 13 | "/usr/lib/zoneinfo", 14 | ]) 15 | .expect("no zoneinfo database"); 16 | 17 | let path_maildir: &str = 18 | get_first_path(&["/var/mail", "/var/spool/mail", "/usr/spool/mail"]).unwrap_or("/var/mail"); 19 | 20 | // TODO: use _PATH_STDPATH and _PATH_DEFPATH_ROOT from paths.h 21 | println!("cargo:rustc-env=SUDO_PATH_DEFAULT=/usr/bin:/bin:/usr/sbin:/sbin"); 22 | println!( 23 | "cargo:rustc-env=SU_PATH_DEFAULT_ROOT=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 24 | ); 25 | println!( 26 | "cargo:rustc-env=SU_PATH_DEFAULT=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games" 27 | ); 28 | 29 | println!("cargo:rustc-env=PATH_MAILDIR={path_maildir}"); 30 | println!("cargo:rustc-env=PATH_ZONEINFO={path_zoneinfo}"); 31 | println!("cargo:rerun-if-changed=build.rs"); 32 | 33 | println!("cargo:rustc-link-lib=pam"); 34 | } 35 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.70" 2 | check-private-items = true 3 | -------------------------------------------------------------------------------- /docs/audit/audit-report-sudo-rs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trifectatechfoundation/sudo-rs/65576dd2db0c22b5678f527e66c310f308e63aff/docs/audit/audit-report-sudo-rs.pdf -------------------------------------------------------------------------------- /docs/man/su.1.man: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 3.6.3 2 | .\" 3 | .TH "SU" "1" "" "sudo\-rs 0.2.6" "sudo\-rs" 4 | .SH NAME 5 | \f[CR]su\f[R] \- run a shell or command as another user 6 | .SH SYNOPSIS 7 | \f[CR]su\f[R] options [\-] [<\f[I]user\f[R]> 8 | [<\f[I]argument\f[R]>\&...]] 9 | .SH OPTIONS 10 | .TP 11 | \f[CR]\-c\f[R] \f[I]command\f[R], \f[CR]\-\-command\f[R]=\f[I]command\f[R] 12 | Pass a single command to the shell with \f[CR]\-c\f[R]. 13 | .TP 14 | \f[CR]\-g\f[R] \f[I]group\f[R], \f[CR]\-\-group\f[R]=\f[I]group\f[R] 15 | Specify the primary group 16 | .TP 17 | \f[CR]\-G\f[R] \f[I]group\f[R], \f[CR]\-\-supp\-group\f[R]=\f[I]group\f[R] 18 | Specify a supplemental group 19 | .TP 20 | \f[CR]\-h\f[R], \f[CR]\-\-help\f[R] 21 | Show a help message. 22 | .TP 23 | \f[CR]\-\f[R], \f[CR]\-l\f[R], \f[CR]\-\-login\f[R] 24 | Make the shell a login shell 25 | .TP 26 | \f[CR]\-m\f[R], \f[CR]\-p\f[R], \f[CR]\-\-preserve\-environment\f[R] 27 | Do not reset environment variables 28 | .TP 29 | \f[CR]\-P\f[R], \f[CR]\-\-pty\f[R] 30 | Create a new pseudo\-terminal when running the shell. 31 | .TP 32 | \f[CR]\-w\f[R] \f[I]list\f[R], \f[CR]\-\-whitelist\-environment\f[R]=\f[I]list\f[R] 33 | Do not reset the environment variables specified by the \f[I]list\f[R]. 34 | Multiple variables can be separated by commas. 35 | .TP 36 | \f[CR]\-s\f[R] \f[I]shell\f[R], \f[CR]\-\-shell\f[R]=\f[I]shell\f[R] 37 | Run \f[I]shell\f[R] if \f[CR]/etc/shells\f[R] allows running as that 38 | shell instead of the default shell for the user. 39 | .TP 40 | \f[CR]\-V\f[R], \f[CR]\-\-version\f[R] 41 | Show the program version. 42 | .SH SEE ALSO 43 | sudo(8) 44 | -------------------------------------------------------------------------------- /docs/man/su.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SU(1) sudo-rs 0.2.6 | sudo-rs 3 | --- 4 | 5 | # NAME 6 | 7 | `su` - run a shell or command as another user 8 | 9 | # SYNOPSIS 10 | 11 | `su` [options] [-] [<*user*> [<*argument*>...]] 12 | 13 | # OPTIONS 14 | 15 | `-c` *command*, `--command`=*command* 16 | : Pass a single command to the shell with `-c`. 17 | 18 | `-g` *group*, `--group`=*group* 19 | : Specify the primary group 20 | 21 | `-G` *group*, `--supp-group`=*group* 22 | : Specify a supplemental group 23 | 24 | `-h`, `--help` 25 | : Show a help message. 26 | 27 | `-`, `-l`, `--login` 28 | : Make the shell a login shell 29 | 30 | `-m`, `-p`, `--preserve-environment` 31 | : Do not reset environment variables 32 | 33 | `-P`, `--pty` 34 | : Create a new pseudo-terminal when running the shell. 35 | 36 | `-w` *list*, `--whitelist-environment`=*list* 37 | : Do not reset the environment variables specified by the *list*. Multiple 38 | variables can be separated by commas. 39 | 40 | `-s` *shell*, `--shell`=*shell* 41 | : Run *shell* if `/etc/shells` allows running as that shell instead of the 42 | default shell for the user. 43 | 44 | `-V`, `--version` 45 | : Show the program version. 46 | 47 | # SEE ALSO 48 | 49 | [sudo(8)](sudo.8.md) 50 | -------------------------------------------------------------------------------- /docs/man/visudo.8.man: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 3.6.3 2 | .\" 3 | .TH "VISUDO" "8" "" "sudo\-rs 0.2.6" "sudo\-rs" 4 | .SH NAME 5 | \f[CR]visudo\f[R] \- safely edit the sudoers file 6 | .SH SYNOPSIS 7 | \f[CR]visudo\f[R] [\f[CR]\-chqsV\f[R]] [[\f[CR]\-f\f[R]] 8 | \f[I]sudoers\f[R]] 9 | .SH DESCRIPTION 10 | \f[CR]visudo\f[R] edits the \f[I]sudoers\f[R] file in a safe manner, 11 | similar to vipw(8). 12 | .SH OPTIONS 13 | .TP 14 | \f[CR]\-c\f[R], \f[CR]\-\-check\f[R] 15 | Only check if there are errors in the existing sudoers file. 16 | .TP 17 | \f[CR]\-f\f[R] \f[I]sudoers\f[R], \f[CR]\-\-file\f[R]=\f[I]sudoers\f[R] 18 | Instead of editing the default \f[CR]/etc/sudoers\f[R], edit the file 19 | specified as \f[I]sudoers\f[R] instead. 20 | .TP 21 | \f[CR]\-h\f[R], \f[CR]\-\-help\f[R] 22 | Show a help message. 23 | .TP 24 | \f[CR]\-I\f[R], \f[CR]\-\-no\-includes\f[R] 25 | Do not edit included files. 26 | .TP 27 | \f[CR]\-q\f[R], \f[CR]\-\-quiet\f[R] 28 | Less verbose syntax error messages. 29 | .TP 30 | \f[CR]\-s\f[R], \f[CR]\-\-strict\f[R] 31 | Strict syntax checking. 32 | .TP 33 | \f[CR]\-V\f[R], \f[CR]\-\-version\f[R] 34 | Display version information and exit. 35 | .SH SEE ALSO 36 | sudo(8), sudoers(5) 37 | -------------------------------------------------------------------------------- /docs/man/visudo.8.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: VISUDO(8) sudo-rs 0.2.6 | sudo-rs 3 | --- 4 | 5 | # NAME 6 | 7 | `visudo` - safely edit the sudoers file 8 | 9 | # SYNOPSIS 10 | 11 | `visudo` [`-chqsV`] [[`-f`] *sudoers*] 12 | 13 | # DESCRIPTION 14 | 15 | `visudo` edits the *sudoers* file in a safe manner, similar to vipw(8). 16 | 17 | # OPTIONS 18 | 19 | `-c`, `--check` 20 | : Only check if there are errors in the existing sudoers file. 21 | 22 | `-f` *sudoers*, `--file`=*sudoers* 23 | : Instead of editing the default `/etc/sudoers`, edit the file specified as 24 | *sudoers* instead. 25 | 26 | `-h`, `--help` 27 | : Show a help message. 28 | 29 | `-I`, `--no-includes` 30 | : Do not edit included files. 31 | 32 | `-q`, `--quiet` 33 | : Less verbose syntax error messages. 34 | 35 | `-s`, `--strict` 36 | : Strict syntax checking. 37 | 38 | `-V`, `--version` 39 | : Display version information and exit. 40 | 41 | # SEE ALSO 42 | 43 | [sudo(8)](sudo.8.md), sudoers(5) 44 | -------------------------------------------------------------------------------- /get-pam-variant.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # FIXME read headers to find the actually used variant 4 | case $(uname) in 5 | Linux) 6 | echo linuxpam 7 | ;; 8 | FreeBSD) 9 | echo openpam 10 | ;; 11 | *) 12 | echo "Unsupported platform" 13 | exit 1 14 | ;; 15 | esac 16 | -------------------------------------------------------------------------------- /make-lcov-info.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | rustup component add llvm-tools 6 | 7 | llvm_profdata=$(find "$(rustc --print sysroot)" -name llvm-profdata) 8 | profdata="$SUDO_TEST_PROFRAW_DIR"/sudo-rs.profdata 9 | $llvm_profdata merge \ 10 | -sparse \ 11 | "$SUDO_TEST_PROFRAW_DIR"/**/*.profraw \ 12 | -o "$profdata" 13 | 14 | binary="$SUDO_TEST_PROFRAW_DIR"/sudo-rs 15 | dockerid=$(docker create sudo-test-rs) 16 | docker cp "$dockerid":/usr/bin/sudo "$binary" 17 | docker rm "$dockerid" 18 | 19 | llvm_cov="$(dirname "$llvm_profdata")"/llvm-cov 20 | $llvm_cov export \ 21 | -format=lcov \ 22 | --ignore-filename-regex='/usr/local/cargo/registry' \ 23 | --ignore-filename-regex='/rustc' \ 24 | --instr-profile="$profdata" \ 25 | --object "$binary" \ 26 | -path-equivalence=/usr/src/sudo,"$(pwd)" >lcov.info 27 | -------------------------------------------------------------------------------- /proofs/sudoers.mlw: -------------------------------------------------------------------------------- 1 | (* Why3 specification file for selected code in the sudoers crate. 2 | All proof goals generated by Why3 1.5.1 can be discharged using CVC4 1.8 *) 3 | 4 | module Sudoers 5 | 6 | use array.Array 7 | use option.Option 8 | use ref.Ref 9 | use int.Int 10 | 11 | (* loose models for the types in sudoers::Ast *) 12 | type metavar 'a = All | Only 'a 13 | type qualified 'a = Yes 'a | No 'a 14 | type spec 'tag 'a = { inner: qualified (metavar 'a); info: 'tag } 15 | 16 | predicate contains (pred: 'a -> bool) (a: array 'a) 17 | = exists i. 0 <= i < length a /\ pred a[i] 18 | 19 | function who (item: spec 'tag 'a): metavar 'a 20 | = match item.inner with 21 | | Yes x -> x 22 | | No x -> x 23 | end 24 | 25 | function condition (item: spec 'tag 'a): option 'tag 26 | = match item with 27 | | { inner = Yes _; info = tag } -> Some tag 28 | | _ -> None 29 | end 30 | 31 | function matched_by (pred: 'a -> bool) (item: spec 'tag 'a): bool 32 | = match who item with 33 | | All -> true 34 | | Only x -> pred x 35 | end 36 | 37 | let function bool_then (b: bool) (x: 'a): option 'a 38 | = if b then Some x else None 39 | 40 | predicate final_match (pred: 'a -> bool) (a: array 'a) (f: 'a -> 'b) (x: 'b) 41 | = exists i. 0 <= i < length a /\ pred a[i] /\ f a[i] = x /\ forall k. i < k < length a -> not pred a[k] 42 | 43 | (* Why3 model of the sudoers::find_item function *) 44 | let find_item (items: array (spec 'tag 'a)) (pred: 'a -> bool): option 'tag 45 | returns { 46 | | Some tag -> final_match (matched_by pred) items condition (Some tag) 47 | | None -> not contains (matched_by pred) items \/ final_match (matched_by pred) items condition None 48 | } 49 | 50 | = let result = ref None in 51 | 52 | for i = 0 to items.length - 1 do 53 | invariant { forall tag. !result = Some tag <-> 54 | exists j. 0 <= j < i /\ matched_by pred items[j] /\ Some tag = condition items[j] /\ 55 | forall k. j < k < i -> not matched_by pred items[k] } 56 | let (judgement, who) = match items[i].inner with 57 | | No x -> (false, x) 58 | | Yes x -> (true, x) 59 | end in 60 | let info = items[i].info in 61 | match who with 62 | | All -> result := judgement.bool_then(info) 63 | | Only id -> if pred id then result := judgement.bool_then(info); 64 | end; 65 | done; 66 | 67 | (* perform a "virtual loop" to strength the case !result = None; 68 | this could also be solved by adding a ghost variable above *) 69 | ghost if is_none !result && contains (matched_by pred) items then 70 | for i = items.length - 1 downto 0 do 71 | invariant { forall k. i < k < items.length -> not matched_by pred items[k] } 72 | invariant { exists k. 0 <= k <= i /\ matched_by pred items[k] } 73 | if matched_by pred items[i] then break 74 | done; 75 | 76 | !result; 77 | 78 | end 79 | -------------------------------------------------------------------------------- /src/apparmor/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::CString, io::ErrorKind}; 2 | 3 | use crate::cutils::cerr; 4 | 5 | mod sys; 6 | 7 | /// Set the profile for the next exec call if AppArmor is enabled 8 | pub fn set_profile_for_next_exec(profile_name: &str) -> std::io::Result<()> { 9 | if apparmor_is_enabled()? { 10 | apparmor_prepare_exec(profile_name) 11 | } else { 12 | // if the sysadmin doesn't have apparmor enabled, fail softly 13 | Ok(()) 14 | } 15 | } 16 | 17 | fn apparmor_is_enabled() -> std::io::Result { 18 | match std::fs::read_to_string("/sys/module/apparmor/parameters/enabled") { 19 | Ok(enabled) => Ok(enabled.starts_with("Y")), 20 | Err(e) if e.kind() == ErrorKind::NotFound => Ok(false), 21 | Err(e) => Err(e), 22 | } 23 | } 24 | 25 | /// Switch the apparmor profile to the given profile on the next exec call 26 | fn apparmor_prepare_exec(new_profile: &str) -> std::io::Result<()> { 27 | let new_profile_cstr = CString::new(new_profile)?; 28 | // SAFETY: new_profile_cstr provided by CString ensures a valid ptr 29 | cerr(unsafe { sys::aa_change_onexec(new_profile_cstr.as_ptr()) })?; 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /src/apparmor/sys.rs: -------------------------------------------------------------------------------- 1 | #[link(name = "apparmor")] 2 | extern "C" { 3 | pub fn aa_change_onexec(profile: *const libc::c_char) -> libc::c_int; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | pub use command::CommandAndArguments; 4 | pub use context::Context; 5 | pub use error::Error; 6 | pub use path::SudoPath; 7 | pub use string::SudoString; 8 | 9 | pub mod bin_serde; 10 | pub mod command; 11 | pub mod context; 12 | pub mod error; 13 | mod path; 14 | pub mod resolve; 15 | mod string; 16 | 17 | // Hardened enum values used for critical enums to mitigate attacks like Rowhammer. 18 | // See for example https://arxiv.org/pdf/2309.02545.pdf 19 | // The values are copied from https://github.com/sudo-project/sudo/commit/7873f8334c8d31031f8cfa83bd97ac6029309e4f#diff-b8ac7ab4c3c4a75aed0bb5f7c5fd38b9ea6c81b7557f775e46c6f8aa115e02cd 20 | pub const HARDENED_ENUM_VALUE_0: u32 = 0x52a2925; // 0101001010100010100100100101 21 | pub const HARDENED_ENUM_VALUE_1: u32 = 0xad5d6da; // 1010110101011101011011011010 22 | pub const HARDENED_ENUM_VALUE_2: u32 = 0x69d61fc8; // 1101001110101100001111111001000 23 | pub const HARDENED_ENUM_VALUE_3: u32 = 0x1629e037; // 0010110001010011110000000110111 24 | pub const HARDENED_ENUM_VALUE_4: u32 = 0x1fc8d3ac; // 11111110010001101001110101100 25 | -------------------------------------------------------------------------------- /src/common/path.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::OsString, 3 | ops, 4 | os::unix::prelude::OsStrExt, 5 | path::{Path, PathBuf}, 6 | str, 7 | }; 8 | 9 | use super::{Error, SudoString}; 10 | 11 | /// A `PathBuf` guaranteed to not contain null bytes and be UTF-8 encoded 12 | #[derive(Clone, Debug, PartialEq)] 13 | #[cfg_attr(test, derive(Eq))] 14 | pub struct SudoPath { 15 | inner: String, 16 | } 17 | 18 | impl SudoPath { 19 | pub fn new(path: PathBuf) -> Result { 20 | let bytes = path.as_os_str().as_bytes(); 21 | if bytes.contains(&0) { 22 | return Err(Error::PathValidation(path)); 23 | } 24 | 25 | // check this through a reference so we can return `path` in the error case 26 | if str::from_utf8(bytes).is_err() { 27 | return Err(Error::PathValidation(path)); 28 | } 29 | 30 | Ok(Self { 31 | // NOTE(unwrap): UTF-8 encoding is checked above 32 | inner: path.into_os_string().into_string().unwrap(), 33 | }) 34 | } 35 | 36 | pub fn from_cli_string(cli_string: impl Into) -> Self { 37 | Self::new(cli_string.into().into()) 38 | .expect("strings that come in from CLI should not have interior null bytes") 39 | } 40 | 41 | /// Resolve the use of a '~' that occurs in this `SudoPathBuf`; based on the sudoers context 42 | pub fn expand_tilde_in_path(&self, default_username: &SudoString) -> Result { 43 | if let Some(prefix) = self.inner.strip_prefix('~') { 44 | let (username, relpath) = prefix.split_once('/').unwrap_or((prefix, "")); 45 | 46 | let username = if username.is_empty() { 47 | default_username.clone() 48 | } else { 49 | SudoString::new(username.to_string()).unwrap() 50 | }; 51 | 52 | let home_dir = crate::system::User::from_name(username.as_cstr()) 53 | .ok() 54 | .flatten() 55 | .ok_or(Error::UserNotFound(username.to_string()))? 56 | .home; 57 | let path = home_dir.join(relpath); 58 | 59 | Self::new(path) 60 | } else { 61 | Ok(self.clone()) 62 | } 63 | } 64 | } 65 | 66 | impl From for PathBuf { 67 | fn from(value: SudoPath) -> Self { 68 | value.inner.into() 69 | } 70 | } 71 | 72 | impl AsRef for SudoPath { 73 | fn as_ref(&self) -> &Path { 74 | self.inner.as_ref() 75 | } 76 | } 77 | 78 | impl ops::Deref for SudoPath { 79 | type Target = Path; 80 | 81 | fn deref(&self) -> &Self::Target { 82 | self.as_ref() 83 | } 84 | } 85 | 86 | impl TryFrom for SudoPath { 87 | type Error = Error; 88 | 89 | fn try_from(value: String) -> Result { 90 | Self::new(value.into()) 91 | } 92 | } 93 | 94 | impl From for OsString { 95 | fn from(value: SudoPath) -> Self { 96 | value.inner.into() 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | impl From<&'_ str> for SudoPath { 102 | fn from(value: &'_ str) -> Self { 103 | Self::new(value.into()).unwrap() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/exec/io_util.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | /// Return `true` if the IO error is an interruption. 4 | pub(super) fn was_interrupted(err: &io::Error) -> bool { 5 | // ogsudo checks against `EINTR` and `EAGAIN`. 6 | matches!( 7 | err.kind(), 8 | io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock 9 | ) 10 | } 11 | 12 | /// Call `f` repeatedly until it succeeds or it encounters a non-interruption error. 13 | pub(super) fn retry_while_interrupted(mut f: impl FnMut() -> io::Result) -> io::Result { 14 | loop { 15 | match f() { 16 | Err(err) if was_interrupted(&err) => {} 17 | result => return result, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/exec/use_pty/mod.rs: -------------------------------------------------------------------------------- 1 | mod backchannel; 2 | mod monitor; 3 | mod parent; 4 | mod pipe; 5 | 6 | use std::ffi::c_int; 7 | 8 | pub(super) use parent::exec_pty; 9 | 10 | use crate::system::signal::SignalNumber; 11 | 12 | /// Continue running in the foreground 13 | pub(super) const SIGCONT_FG: SignalNumber = -2; 14 | /// Continue running in the background 15 | pub(super) const SIGCONT_BG: SignalNumber = -3; 16 | 17 | enum CommandStatus { 18 | Exit(c_int), 19 | Term(SignalNumber), 20 | Stop(SignalNumber), 21 | } 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod macros; 3 | #[cfg(feature = "apparmor")] 4 | pub(crate) mod apparmor; 5 | pub(crate) mod common; 6 | pub(crate) mod cutils; 7 | pub(crate) mod defaults; 8 | pub(crate) mod exec; 9 | pub(crate) mod log; 10 | pub(crate) mod pam; 11 | pub(crate) mod sudoers; 12 | pub(crate) mod system; 13 | 14 | mod su; 15 | mod sudo; 16 | mod visudo; 17 | 18 | pub use su::main as su_main; 19 | pub use sudo::main as sudo_main; 20 | pub use visudo::main as visudo_main; 21 | 22 | #[cfg(feature = "do-not-use-all-features")] 23 | compile_error!("Refusing to compile using 'cargo --all-features' --- please read the README"); 24 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | // the `std::print` macros panic on any IO error. these are non-panicking alternatives 2 | macro_rules! println_ignore_io_error { 3 | ($($tt:tt)*) => {{ 4 | use std::io::Write; 5 | let _ = writeln!(std::io::stdout(), $($tt)*); 6 | }} 7 | } 8 | 9 | macro_rules! eprintln_ignore_io_error { 10 | ($($tt:tt)*) => {{ 11 | use std::io::Write; 12 | let _ = writeln!(std::io::stderr(), $($tt)*); 13 | }} 14 | } 15 | 16 | // catch unintentional uses of `print*` macros with the test suite 17 | #[allow(unused_macros)] 18 | #[cfg(debug_assertions)] 19 | macro_rules! eprintln { 20 | ($($tt:tt)*) => { 21 | compile_error!("do not use `eprintln!`; use the `write!` macro instead") 22 | }; 23 | } 24 | 25 | #[allow(unused_macros)] 26 | #[cfg(debug_assertions)] 27 | macro_rules! eprint { 28 | ($($tt:tt)*) => { 29 | compile_error!("do not use `eprint!`; use the `write!` macro instead") 30 | }; 31 | } 32 | 33 | #[allow(unused_macros)] 34 | #[cfg(debug_assertions)] 35 | macro_rules! println { 36 | ($($tt:tt)*) => { 37 | compile_error!("do not use `println!`; use the `write!` macro instead") 38 | }; 39 | } 40 | 41 | #[allow(unused_macros)] 42 | #[cfg(debug_assertions)] 43 | macro_rules! print { 44 | ($($tt:tt)*) => { 45 | compile_error!("do not use `print!`; use the `write!` macro instead") 46 | }; 47 | } 48 | 49 | macro_rules! cstr { 50 | ($lit:literal) => {{ 51 | // this `const` item produces compile time errors = it performs the checks at compile time 52 | const CS: &'static std::ffi::CStr = 53 | match std::ffi::CStr::from_bytes_until_nul(concat!($lit, "\0").as_bytes()) { 54 | Ok(x) => x, 55 | Err(_) => panic!("string literal did not pass CStr checks"), 56 | }; 57 | CS 58 | }}; 59 | } 60 | -------------------------------------------------------------------------------- /src/pam/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /src/su/help.rs: -------------------------------------------------------------------------------- 1 | pub const USAGE_MSG: &str = "Usage: su [options] [-] [ [...]]"; 2 | 3 | const DESCRIPTOR: &str = "Change the effective user ID and group ID to that of . 4 | A mere - implies -l. If is not given, root is assumed."; 5 | 6 | const HELP_MSG: &str = "Options: 7 | -m, -p, --preserve-environment do not reset environment variables 8 | -w, --whitelist-environment don't reset specified variables 9 | 10 | -g, --group specify the primary group 11 | -G, --supp-group specify a supplemental group 12 | 13 | -, -l, --login make the shell a login shell 14 | -c, --command pass a single command to the shell with -c 15 | -s, --shell run if /etc/shells allows it 16 | -P, --pty create a new pseudo-terminal 17 | 18 | -h, --help display this help 19 | -V, --version display version 20 | "; 21 | 22 | pub fn long_help_message() -> String { 23 | format!("{USAGE_MSG}\n\n{DESCRIPTOR}\n\n{HELP_MSG}") 24 | } 25 | -------------------------------------------------------------------------------- /src/sudo/cli/help.rs: -------------------------------------------------------------------------------- 1 | pub const USAGE_MSG: &str = "\ 2 | usage: sudo -h | -K | -k | -V 3 | usage: sudo -v [-BknS] [-g group] [-u user] 4 | usage: sudo -l [-BknS] [-g group] [-U user] [-u user] [command [arg ...]] 5 | usage: sudo [-BknS] [-D directory] [-g group] [-u user] [-i | -s] [command [arg ...]] 6 | usage: sudo -e [-BknS] [-D directory] [-g group] [-u user] file ..."; 7 | 8 | const DESCRIPTOR: &str = "sudo - run commands as another user"; 9 | 10 | const HELP_MSG: &str = "Options: 11 | -B, --bell ring bell when prompting 12 | -D, --chdir=directory change the working directory before running command 13 | -g, --group=group run command as the specified group name or ID 14 | -h, --help display help message and exit 15 | -i, --login run login shell as the target user; a command may also be specified 16 | -K, --remove-timestamp remove timestamp file completely 17 | -k, --reset-timestamp invalidate timestamp file 18 | -l, --list list user's privileges or check a specific command; use twice for longer format 19 | -n, --non-interactive non-interactive mode, no prompts are used 20 | -S, --stdin read password from standard input 21 | -s, --shell run shell as the target user; a command may also be specified 22 | -U, --other-user=user in list mode, display privileges for user 23 | -u, --user=user run command (or edit file) as specified user name or ID 24 | -V, --version display version information and exit 25 | -v, --validate update user's timestamp without running a command 26 | -- stop processing command line arguments"; 27 | 28 | pub fn long_help_message() -> String { 29 | format!("{DESCRIPTOR}\n{USAGE_MSG}\n{HELP_MSG}") 30 | } 31 | -------------------------------------------------------------------------------- /src/sudo/diagnostic.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{BufRead, BufReader}; 3 | use std::path::Path; 4 | 5 | use crate::sudoers::Span; 6 | 7 | pub(crate) fn cited_error(message: &str, span: Span, path: impl AsRef) { 8 | let path_str = path.as_ref().display(); 9 | let Span { 10 | start: (line, col), 11 | end: (end_line, mut end_col), 12 | } = span; 13 | eprintln_ignore_io_error!("{path_str}:{line}:{col}: {message}"); 14 | 15 | // we won't try to "span" errors across multiple lines 16 | if line != end_line { 17 | end_col = col; 18 | } 19 | 20 | let citation = || { 21 | let inp = BufReader::new(File::open(path).ok()?); 22 | let line = inp.lines().nth(line - 1)?.ok()?; 23 | let padding = line 24 | .chars() 25 | .take(col - 1) 26 | .map(|c| if c.is_whitespace() { c } else { ' ' }) 27 | .collect::(); 28 | let lineunder = std::iter::repeat('~') 29 | .take(end_col - col) 30 | .skip(1) 31 | .collect::(); 32 | eprintln_ignore_io_error!("{line}"); 33 | eprintln_ignore_io_error!("{padding}^{lineunder}"); 34 | Some(()) 35 | }; 36 | 37 | // we ignore any errors in displaying an error 38 | let _ = citation(); 39 | } 40 | 41 | macro_rules! diagnostic { 42 | ($str:expr, $path:tt @ $pos:ident) => { 43 | if let Some(range) = $pos { 44 | $crate::sudo::diagnostic::cited_error(&format!($str), range, $path); 45 | } else { 46 | eprintln_ignore_io_error!("sudo-rs: {}", format!($str)); 47 | } 48 | }; 49 | ($str:expr) => {{ 50 | eprintln_ignore_io_error!("sudo-rs: {}", format!($str)); 51 | }}; 52 | } 53 | 54 | pub(crate) use diagnostic; 55 | -------------------------------------------------------------------------------- /src/sudo/env/mod.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | pub mod environment; 4 | pub mod wildcard_match; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | -------------------------------------------------------------------------------- /src/sudo/env/wildcard_match.rs: -------------------------------------------------------------------------------- 1 | /// Match a test input with a pattern 2 | /// Only wildcard characters (*) in the pattern string have a special meaning: they match on zero or more characters 3 | pub(super) fn wildcard_match(test: &[u8], pattern: &[u8]) -> bool { 4 | let mut test_index = 0; 5 | let mut pattern_index = 0; 6 | let mut last_star = None; 7 | 8 | loop { 9 | match (pattern.get(pattern_index), test.get(test_index)) { 10 | (Some(p), Some(t)) => { 11 | if *p == b'*' { 12 | pattern_index += 1; 13 | last_star = Some((test_index, pattern_index)); 14 | } else if p == t { 15 | pattern_index += 1; 16 | test_index += 1; 17 | } else if let Some((t_index, p_index)) = last_star { 18 | test_index = t_index + 1; 19 | pattern_index = p_index; 20 | last_star = Some((test_index, pattern_index)); 21 | } else { 22 | return false; 23 | } 24 | } 25 | (None, Some(_)) => { 26 | if let Some((t_index, p_index)) = last_star { 27 | test_index = t_index + 1; 28 | pattern_index = p_index; 29 | last_star = Some((test_index, pattern_index)); 30 | } else { 31 | return false; 32 | } 33 | } 34 | (Some(b'*'), None) => { 35 | pattern_index += 1; 36 | } 37 | (None, None) => { 38 | return true; 39 | } 40 | _ => { 41 | return false; 42 | } 43 | } 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::wildcard_match; 50 | 51 | #[test] 52 | fn test_wildcard_match() { 53 | let tests = vec![ 54 | ("foo bar", "foo *", true), 55 | ("foo bar", "foo ba*", true), 56 | ("foo bar", "foo *ar", true), 57 | ("foo bar", "foo *r", true), 58 | ("foo bar", "foo *ab", false), 59 | ("foo bar", "foo r*", false), 60 | ("foo bar", "*oo bar", true), 61 | ("foo bar", "*f* bar", true), 62 | ("foo bar", "*f bar", false), 63 | ("foo ", "foo *", true), 64 | ("foo", "foo *", false), 65 | ("foo", "foo*", true), 66 | ("foo bar", "f*******r", true), 67 | ("foo******bar", "f*r", true), 68 | ("foo********bar", "foo bar", false), 69 | ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#*t13%#@$%#$%", true), 70 | ("#%^$V@#TYH%&rot13%#@$%#$%", "*%^*%&rot*%#$%", true), 71 | ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#TYH%&r*%#@$#$%", false), 72 | ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#*******@$%#$%", true), 73 | ]; 74 | 75 | for (test, pattern, expected) in tests.into_iter() { 76 | assert_eq!( 77 | wildcard_match(test.as_bytes(), pattern.as_bytes()), 78 | expected, 79 | "\"{}\" {} match {}", 80 | test, 81 | if expected { "should" } else { "should not" }, 82 | pattern 83 | ); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/sudoers/char_stream.rs: -------------------------------------------------------------------------------- 1 | pub struct CharStream<'a> { 2 | iter: std::iter::Peekable>, 3 | line: usize, 4 | col: usize, 5 | } 6 | 7 | /// Advance the given position by `n` horizontal steps 8 | pub fn advance(pos: (usize, usize), n: usize) -> (usize, usize) { 9 | (pos.0, pos.1 + n) 10 | } 11 | 12 | impl<'a> CharStream<'a> { 13 | pub fn new_with_pos(src: &'a str, (line, col): (usize, usize)) -> Self { 14 | CharStream { 15 | iter: src.chars().peekable(), 16 | line, 17 | col, 18 | } 19 | } 20 | 21 | pub fn new(src: &'a str) -> Self { 22 | Self::new_with_pos(src, (1, 1)) 23 | } 24 | } 25 | 26 | impl CharStream<'_> { 27 | pub fn next_if(&mut self, f: impl FnOnce(char) -> bool) -> Option { 28 | let item = self.iter.next_if(|&c| f(c)); 29 | match item { 30 | Some('\n') => { 31 | self.line += 1; 32 | self.col = 1; 33 | } 34 | Some(_) => self.col += 1, 35 | _ => {} 36 | } 37 | item 38 | } 39 | 40 | pub fn eat_char(&mut self, expect_char: char) -> bool { 41 | self.next_if(|c| c == expect_char).is_some() 42 | } 43 | 44 | pub fn skip_to_newline(&mut self) { 45 | while self.next_if(|c| c != '\n').is_some() {} 46 | } 47 | 48 | pub fn peek(&mut self) -> Option { 49 | self.iter.peek().cloned() 50 | } 51 | 52 | pub fn get_pos(&self) -> (usize, usize) { 53 | (self.line, self.col) 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod test { 59 | use super::*; 60 | 61 | #[test] 62 | fn test_iter() { 63 | let mut stream = CharStream::new("12\n3\n"); 64 | assert_eq!(stream.peek(), Some('1')); 65 | assert!(stream.eat_char('1')); 66 | assert_eq!(stream.peek(), Some('2')); 67 | assert!(stream.eat_char('2')); 68 | assert!(stream.eat_char('\n')); 69 | assert_eq!(stream.peek(), Some('3')); 70 | assert!(stream.eat_char('3')); 71 | assert_eq!(stream.get_pos(), (2, 2)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/sudoers/entry/verbose.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use crate::sudoers::{ 4 | ast::{Authenticate, RunAs, Tag}, 5 | tokens::ChDir, 6 | }; 7 | 8 | use super::Entry; 9 | 10 | pub struct Verbose<'a>(pub Entry<'a>); 11 | 12 | impl fmt::Display for Verbose<'_> { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | let Self(Entry { 15 | run_as, 16 | cmd_specs, 17 | cmd_alias, 18 | }) = self; 19 | 20 | let root_runas = super::root_runas(); 21 | let run_as = run_as.unwrap_or(&root_runas); 22 | 23 | let mut last_tag = None; 24 | for (tag, cmd_spec) in cmd_specs { 25 | if last_tag != Some(tag) { 26 | let is_first_iteration = last_tag.is_none(); 27 | if !is_first_iteration { 28 | f.write_str("\n")?; 29 | } 30 | 31 | write_entry_header(run_as, f)?; 32 | write_tag(f, tag)?; 33 | f.write_str("\n Commands:")?; 34 | } 35 | last_tag = Some(tag); 36 | 37 | f.write_str("\n\t")?; 38 | super::write_spec(f, cmd_spec, cmd_alias.iter().rev(), true, "\n\t")?; 39 | } 40 | 41 | Ok(()) 42 | } 43 | } 44 | 45 | fn write_entry_header(run_as: &RunAs, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | f.write_str("\nSudoers entry:")?; 47 | 48 | write_users(run_as, f)?; 49 | write_groups(run_as, f) 50 | } 51 | 52 | fn write_users(run_as: &RunAs, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | f.write_str("\n RunAsUsers: ")?; 54 | super::write_users(run_as, f) 55 | } 56 | 57 | fn write_groups(run_as: &RunAs, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | if run_as.groups.is_empty() { 59 | return Ok(()); 60 | } 61 | 62 | f.write_str("\n RunAsGroups: ")?; 63 | super::write_groups(run_as, f) 64 | } 65 | 66 | fn write_tag(f: &mut fmt::Formatter, tag: &Tag) -> fmt::Result { 67 | if tag.authenticate != Authenticate::None { 68 | f.write_str("\n Options: ")?; 69 | if tag.authenticate != Authenticate::Passwd { 70 | f.write_str("!")?; 71 | } 72 | f.write_str("authenticate")?; 73 | } 74 | 75 | if let Some(cwd) = &tag.cwd { 76 | f.write_str("\n Cwd: ")?; 77 | match cwd { 78 | ChDir::Path(path) => write!(f, "{}", path.display())?, 79 | ChDir::Any => f.write_str("*")?, 80 | } 81 | } 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /src/system/file/chown.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io, os::fd::AsRawFd}; 2 | 3 | use crate::{ 4 | cutils::cerr, 5 | system::interface::{GroupId, UserId}, 6 | }; 7 | 8 | pub(crate) trait Chown { 9 | fn chown(&self, uid: UserId, gid: GroupId) -> io::Result<()>; 10 | } 11 | 12 | impl Chown for File { 13 | fn chown(&self, owner: UserId, group: GroupId) -> io::Result<()> { 14 | let fd = self.as_raw_fd(); 15 | 16 | // SAFETY: `fchown` is passed a proper file descriptor; and even if the user/group id 17 | // is invalid, it will not cause UB. 18 | cerr(unsafe { libc::fchown(fd, owner.inner(), group.inner()) }).map(|_| ()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/system/file/lock.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Result, 4 | os::fd::{AsRawFd, RawFd}, 5 | }; 6 | 7 | use crate::cutils::cerr; 8 | 9 | pub(crate) struct FileLock { 10 | fd: RawFd, 11 | } 12 | 13 | impl FileLock { 14 | /// Get an exclusive lock on the file, waits if there is currently a lock 15 | /// on the file if `nonblocking` is true. 16 | pub(crate) fn exclusive(file: &File, nonblocking: bool) -> Result { 17 | let fd = file.as_raw_fd(); 18 | flock(fd, LockOp::LockExclusive, nonblocking)?; 19 | Ok(Self { fd }) 20 | } 21 | 22 | /// Release the lock on the file. 23 | pub(crate) fn unlock(self) -> Result<()> { 24 | flock(self.fd, LockOp::Unlock, false) 25 | } 26 | } 27 | 28 | impl Drop for FileLock { 29 | fn drop(&mut self) { 30 | flock(self.fd, LockOp::Unlock, false).ok(); 31 | } 32 | } 33 | 34 | #[derive(Clone, Copy, Debug)] 35 | enum LockOp { 36 | LockExclusive, 37 | Unlock, 38 | } 39 | 40 | impl LockOp { 41 | fn as_flock_operation(self) -> libc::c_int { 42 | match self { 43 | LockOp::LockExclusive => libc::LOCK_EX, 44 | LockOp::Unlock => libc::LOCK_UN, 45 | } 46 | } 47 | } 48 | 49 | fn flock(fd: RawFd, action: LockOp, nonblocking: bool) -> Result<()> { 50 | let mut operation = action.as_flock_operation(); 51 | if nonblocking { 52 | operation |= libc::LOCK_NB; 53 | } 54 | 55 | // SAFETY: even if `fd` would not be a valid file descriptor, that would merely 56 | // result in an error condition, not UB 57 | cerr(unsafe { libc::flock(fd, operation) })?; 58 | Ok(()) 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use crate::system::tests::tempfile; 64 | 65 | use super::*; 66 | 67 | #[test] 68 | fn test_locking_of_tmp_file() { 69 | let f = tempfile().unwrap(); 70 | 71 | FileLock::exclusive(&f, false).unwrap().unlock().unwrap(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/system/file/mod.rs: -------------------------------------------------------------------------------- 1 | mod chown; 2 | mod lock; 3 | mod tmpdir; 4 | 5 | pub(crate) use chown::Chown; 6 | pub(crate) use lock::FileLock; 7 | pub(crate) use tmpdir::create_temporary_dir; 8 | -------------------------------------------------------------------------------- /src/system/file/tmpdir.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CString, OsString}; 2 | use std::io; 3 | use std::os::unix::ffi::OsStringExt; 4 | use std::path::PathBuf; 5 | 6 | pub(crate) fn create_temporary_dir() -> io::Result { 7 | let template = cstr!("/tmp/sudoers-XXXXXX").to_owned(); 8 | 9 | // SAFETY: mkdtemp is passed a valid null-terminated C string 10 | let ptr = unsafe { libc::mkdtemp(template.into_raw()) }; 11 | 12 | if ptr.is_null() { 13 | return Err(io::Error::last_os_error()); 14 | } 15 | 16 | // SAFETY: ptr is the same pointer produced by into_raw() above, and it 17 | // is still pointing to a zero-terminated C string 18 | let path = OsString::from_vec(unsafe { CString::from_raw(ptr) }.into_bytes()).into(); 19 | 20 | Ok(path) 21 | } 22 | -------------------------------------------------------------------------------- /src/system/signal/handler.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crate::log::dev_warn; 4 | 5 | use super::{consts::*, set::SignalAction, signal_name, SignalNumber}; 6 | 7 | /// A handler for a signal. 8 | /// 9 | /// When a value of this type is dropped, it will try to restore the action that was registered for 10 | /// the signal prior to calling [`SignalHandler::register`]. 11 | pub(crate) struct SignalHandler { 12 | signal: SignalNumber, 13 | original_action: SignalAction, 14 | } 15 | 16 | impl SignalHandler { 17 | const FORBIDDEN: &'static [SignalNumber] = &[SIGKILL, SIGSTOP]; 18 | 19 | /// Register a new handler for the given signal with the provided behavior. 20 | /// 21 | /// # Panics 22 | /// 23 | /// If it is not possible to override the action for the provided signal. 24 | pub(crate) fn register( 25 | signal: SignalNumber, 26 | behavior: SignalHandlerBehavior, 27 | ) -> io::Result { 28 | if Self::FORBIDDEN.contains(&signal) { 29 | panic!( 30 | "the {} signal action cannot be overridden", 31 | signal_name(signal) 32 | ); 33 | } 34 | 35 | let action = SignalAction::new(behavior)?; 36 | let original_action = action.register(signal)?; 37 | 38 | Ok(Self { 39 | signal, 40 | original_action, 41 | }) 42 | } 43 | 44 | /// Forget this signal handler. 45 | /// 46 | /// This can be used to avoid restoring the original action for the signal. 47 | pub(crate) fn forget(self) { 48 | std::mem::forget(self) 49 | } 50 | } 51 | 52 | impl Drop for SignalHandler { 53 | #[track_caller] 54 | fn drop(&mut self) { 55 | let signal = self.signal; 56 | if let Err(err) = self.original_action.register(signal) { 57 | dev_warn!( 58 | "cannot restore original action for {}: {err}", 59 | signal_name(signal), 60 | ) 61 | } 62 | } 63 | } 64 | 65 | /// The possible behaviors for a [`SignalHandler`]. 66 | pub(crate) enum SignalHandlerBehavior { 67 | /// Execute the default action for the signal. 68 | Default, 69 | /// Ignore the arrival of the signal. 70 | Ignore, 71 | /// Stream the signal information into the latest initialized instance of [`super::SignalStream`]. 72 | Stream, 73 | } 74 | -------------------------------------------------------------------------------- /src/system/signal/info.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::system::interface::ProcessId; 4 | 5 | use super::SignalNumber; 6 | 7 | /// Information related to the arrival of a signal. 8 | #[repr(transparent)] 9 | pub(crate) struct SignalInfo { 10 | info: libc::siginfo_t, 11 | } 12 | 13 | impl SignalInfo { 14 | pub(super) const SIZE: usize = std::mem::size_of::(); 15 | 16 | /// Returns whether the signal was sent by the user or not. 17 | fn is_user_signaled(&self) -> bool { 18 | // This matches the definition of the SI_FROMUSER macro. 19 | self.info.si_code <= 0 20 | } 21 | 22 | /// Gets the PID that sent the signal. 23 | pub(crate) fn signaler_pid(&self) -> Option { 24 | if self.is_user_signaled() { 25 | // SAFETY: si_pid is always initialized if the signal is user signaled. 26 | let id = unsafe { self.info.si_pid() }; 27 | Some(ProcessId::new(id)) 28 | } else { 29 | None 30 | } 31 | } 32 | 33 | /// Gets the signal number. 34 | pub(crate) fn signal(&self) -> SignalNumber { 35 | self.info.si_signo 36 | } 37 | } 38 | 39 | impl fmt::Display for SignalInfo { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | write!( 42 | f, 43 | "{} {} from ", 44 | if self.is_user_signaled() { 45 | " user signaled" 46 | } else { 47 | "" 48 | }, 49 | self.signal(), 50 | )?; 51 | if let Some(pid) = self.signaler_pid() { 52 | write!(f, "{pid}") 53 | } else { 54 | write!(f, "") 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/system/signal/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to handle signals. 2 | mod handler; 3 | mod info; 4 | mod set; 5 | mod stream; 6 | 7 | pub(crate) use handler::{SignalHandler, SignalHandlerBehavior}; 8 | pub(crate) use set::SignalSet; 9 | pub(crate) use stream::{register_handlers, SignalStream}; 10 | 11 | use std::borrow::Cow; 12 | 13 | pub(crate) type SignalNumber = libc::c_int; 14 | 15 | macro_rules! define_consts { 16 | ($($signal:ident,)*) => { 17 | pub(crate) mod consts { 18 | pub(crate) use libc::{$($signal,)*}; 19 | } 20 | 21 | pub(crate) fn signal_name(signal: SignalNumber) -> Cow<'static, str> { 22 | match signal { 23 | $(consts::$signal => stringify!($signal).into(),)* 24 | _ => format!("unknown signal ({signal})").into(), 25 | } 26 | } 27 | }; 28 | } 29 | 30 | define_consts! { 31 | SIGINT, 32 | SIGQUIT, 33 | SIGTSTP, 34 | SIGTERM, 35 | SIGHUP, 36 | SIGALRM, 37 | SIGPIPE, 38 | SIGUSR1, 39 | SIGUSR2, 40 | SIGCHLD, 41 | SIGCONT, 42 | SIGWINCH, 43 | SIGTTIN, 44 | SIGTTOU, 45 | SIGKILL, 46 | SIGSTOP, 47 | } 48 | -------------------------------------------------------------------------------- /src/visudo/help.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const USAGE_MSG: &str = "usage: visudo [-chqsV] [[-f] sudoers ]"; 2 | 3 | const DESCRIPTOR: &str = "visudo - safely edit the sudoers file"; 4 | 5 | const HELP_MSG: &str = "Options: 6 | -c, --check check-only mode 7 | -f, --file=sudoers specify sudoers file location 8 | -h, --help display help message and exit 9 | -I, --no-includes do not edit include files 10 | -q, --quiet less verbose (quiet) syntax error messages 11 | -s, --strict strict syntax checking 12 | -V, --version display version information and exit 13 | "; 14 | 15 | pub(crate) fn long_help_message() -> String { 16 | format!("{USAGE_MSG}\n\n{DESCRIPTOR}\n\n{HELP_MSG}") 17 | } 18 | -------------------------------------------------------------------------------- /test-framework/.gitignore: -------------------------------------------------------------------------------- 1 | *.snap.new 2 | /target 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /test-framework/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["sudo-test", "sudo-compliance-tests", "e2e-tests"] 3 | resolver = "2" 4 | 5 | [profile.dev.package.insta] 6 | opt-level = 3 7 | 8 | [profile.dev.package.similar] 9 | opt-level = 3 10 | -------------------------------------------------------------------------------- /test-framework/README.md: -------------------------------------------------------------------------------- 1 | # Compliance tests 2 | 3 | This directory contains compliance tests where we check that the `sudo-rs` command line tool behaves as the original `sudo` for the use cases that we support. 4 | It also contains end-to-end (E2E) tests; these tests are _not_ run against the original `sudo` but use the same test framework and helpers as the compliance tests. 5 | 6 | ## Dependencies 7 | 8 | To run these tests you need to have docker and docker-buildx installed and the docker daemon must be running. 9 | On Arch Linux you can install the relevant packages with the following command: 10 | 11 | ```console 12 | $ sudo pacman -S docker docker-buildx 13 | ``` 14 | 15 | ## Running the tests 16 | 17 | To run all the compliance tests against the original sudo execute the following command from this directory: 18 | 19 | ```console 20 | $ cargo test -p sudo-compliance-tests -- --include-ignored 21 | ``` 22 | 23 | To run the "gated" compliance tests against sudo-rs set the `SUDO_UNDER_TEST` variable to `ours` before invoking Cargo: 24 | 25 | ```console 26 | $ SUDO_UNDER_TEST=ours cargo test -p sudo-compliance-tests 27 | ``` 28 | 29 | To run the E2E tests, you must set the `SUDO_UNDER_TEST` variable to `ours`: 30 | 31 | ```console 32 | $ SUDO_UNDER_TEST=ours cargo test -p e2e-tests 33 | ``` 34 | 35 | ## Gating CI on selected tests 36 | 37 | Tests (`#[test]` functions) that exercise behavior not yet implemented in sudo-rs MUST be marked as `#[ignored]`. 38 | When said behavior is implemented in sudo-rs, the `#[ignored]` attribute MUST be removed from the test. 39 | CI will run `#[ignored]` tests against sudo-rs and fail the build if any of them passes -- as that indicates that an `#[ignored]` attribute was not removed. 40 | 41 | ## Using docker containers as a sudo playground 42 | 43 | ### Original sudo 44 | 45 | _After_ you have run `cargo t -p sudo-compliance-tests` you'll be able to spin up a container based on the docker image used to run the tests against the original sudo with the following command: 46 | 47 | ```console 48 | $ docker run --rm -it sudo-test-og 49 | 50 | root@5b3a062d6dcc:/# 51 | ``` 52 | 53 | After you have an active container you can spawn shells as regular users using the following command 54 | 55 | ```console 56 | $ docker exec -u 1000:100 -it 5b3a062d6dcc bash 57 | 58 | I have no name!@5b3a062d6dcc:/$ 59 | ``` 60 | 61 | `1000` is the user ID you'll assume and `100` is the group ID. `5b3a062d6dcc` is the ID of the docker container. You can get the ID from either the shell prompt you get after running `docker run` or from `docker ps`. 62 | 63 | Note that terminating the `docker run` shell will terminate all the `docker exec` shells. The docker container will be removed after the `docker run` shell is terminated. 64 | 65 | ### sudo-rs 66 | 67 | _After_ you have run `SUDO_UNDER_TEST=ours cargo t -p sudo-compliance-tests` a Docker image that has a source build of `sudo-rs` installed as the system sudo (`/usr/src/bin/sudo`) will become available. The docker image is named `sudo-test-rs`. 68 | 69 | All the instructions in the previous subsection can be used to test `sudo-rs` within a docker container: simply use `sudo-test-rs` instead of `sudo-test-og` as the docker image name. 70 | 71 | ```console 72 | $ docker run --rm -it sudo-test-rs 73 | 74 | root@b5ee5351b9c6:/usr/src/sudo# 75 | ``` 76 | 77 | ```console 78 | $ docker exec -u 1000:100 -it b5ee5351b9c6 bash 79 | 80 | I have no name!@b5ee5351b9c6:/usr/src/sudo$ 81 | ``` 82 | -------------------------------------------------------------------------------- /test-framework/e2e-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "e2e-tests" 4 | publish = false 5 | version = "0.0.0" 6 | 7 | [dependencies] 8 | sudo-test.path = "../sudo-test" 9 | -------------------------------------------------------------------------------- /test-framework/e2e-tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | mod pty; 4 | mod regression; 5 | mod su; 6 | 7 | const USERNAME: &str = "ferris"; 8 | 9 | #[test] 10 | fn sanity_check() { 11 | assert!( 12 | !sudo_test::is_original_sudo(), 13 | "you must set `SUDO_UNDER_TEST=ours` when running this test suite" 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /test-framework/e2e-tests/src/su.rs: -------------------------------------------------------------------------------- 1 | mod flag_pty; 2 | mod signal_handling; 3 | -------------------------------------------------------------------------------- /test-framework/e2e-tests/src/su/signal_handling/expects-signal.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | trap 'echo got signal && exit 0' "$@" 4 | for _ in $(seq 1 20); do 5 | sleep 0.1 6 | done 7 | echo >&2 received no signal 8 | exit 1 9 | -------------------------------------------------------------------------------- /test-framework/e2e-tests/src/su/signal_handling/kill-su-parent.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | trap 'echo received SIGTERM; exit 11' TERM 4 | su_pid="$(pidof su)" 5 | [ -n "$su_pid" ] || (echo su process not found && exit 22) 6 | kill "$su_pid" 7 | 8 | # as insurance, wait a bit for signal delivery to happen 9 | # the signal handler won't run while `sleep` runs hence the multiple `sleep` invocations 10 | for _ in $(seq 1 10); do 11 | sleep 0.1 12 | done 13 | -------------------------------------------------------------------------------- /test-framework/e2e-tests/src/su/signal_handling/kill-su.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # because the su process is `spawn`-ed it may not be immediately visible so 4 | # retry `pidof` until it becomes visible 5 | for _ in $(seq 1 20); do 6 | # when su runs with `use_pty` there are two su processes as su spawns 7 | # a monitor process. We want the PID of the su process so we assume it 8 | # must be the smallest of the returned PIDs. 9 | supid="-1" 10 | pids="$(pidof su)" 11 | for pid in $pids; do 12 | if [ "$pid" -le "$supid" ] || [ "$supid" -eq -1 ]; then 13 | supid=$pid 14 | fi 15 | done 16 | 17 | if [ "$supid" -ne -1 ]; then 18 | # give `expects-signal.sh ` some time to execute the `trap` command 19 | # otherwise it'll be terminated before the signal handler is installed 20 | sleep 0.1 21 | kill "$1" "$supid" 22 | exit 0 23 | fi 24 | sleep 0.1 25 | done 26 | 27 | echo >&2 timeout 28 | exit 1 29 | -------------------------------------------------------------------------------- /test-framework/e2e-tests/src/su/signal_handling/sigtstp.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | # enable 'job control' to make `fg` work 5 | set -m 6 | 7 | su -c "exec sh -c 'for i in "'$(seq 1 5)'"; do date +%s; sleep 1; done'" & 8 | sleep 2 9 | kill -TSTP $! 10 | sleep 5 11 | fg 12 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sudo-compliance-tests" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | doctest = false 8 | 9 | [dev-dependencies] 10 | pretty_assertions = "1.3.0" 11 | insta = { version = "1.29.0", features = [ "filters" ] } 12 | sudo-test.path = "../sudo-test" 13 | 14 | [features] 15 | default = [] 16 | apparmor = ["sudo-test/apparmor"] 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use sudo_test::{Child, Command, Env}; 4 | 5 | #[track_caller] 6 | pub fn parse_env_output(env_output: &str) -> HashMap<&str, &str> { 7 | let mut env = HashMap::new(); 8 | for line in env_output.lines() { 9 | if let Some((key, value)) = line.split_once('=') { 10 | env.insert(key, value); 11 | } else { 12 | panic!("invalid env syntax: {line}"); 13 | } 14 | } 15 | 16 | env 17 | } 18 | 19 | pub fn parse_path(path: &str) -> HashSet<&str> { 20 | path.split(':').collect() 21 | } 22 | 23 | pub struct Rsyslogd<'a> { 24 | _child: Child, 25 | env: &'a Env, 26 | } 27 | 28 | impl<'a> Rsyslogd<'a> { 29 | pub fn start(env: &'a Env) -> Self { 30 | let child = Command::new("rsyslogd").arg("-n").spawn(env); 31 | Self { _child: child, env } 32 | } 33 | 34 | /// returns the contents of `/var/auth.log` 35 | #[track_caller] 36 | pub fn auth_log(&self) -> String { 37 | let path = "/var/log/auth.log"; 38 | Command::new("sh") 39 | .arg("-c") 40 | .arg(format!("[ ! -f {path} ] || cat {path}")) 41 | .output(self.env) 42 | .stdout() 43 | } 44 | } 45 | 46 | impl Drop for Rsyslogd<'_> { 47 | fn drop(&mut self) { 48 | // need to kill the daemon or `Env::drop` won't properly `stop` the docker container 49 | let _ = Command::new("sh") 50 | .args(["-c", "kill -9 $(pidof rsyslogd) || true"]) 51 | .output(self.env); 52 | } 53 | } 54 | 55 | #[test] 56 | #[cfg_attr( 57 | target_os = "freebsd", 58 | ignore = "Logging not really functional on FreeBSD even with og-sudo" 59 | )] 60 | fn rsyslogd_works() { 61 | let env = Env("").build(); 62 | let rsyslog = Rsyslogd::start(&env); 63 | 64 | let auth_log = rsyslog.auth_log(); 65 | assert_eq!("", auth_log); 66 | 67 | Command::new("useradd") 68 | .arg("ferris") 69 | .output(&env) 70 | .assert_success(); 71 | 72 | let auth_log = rsyslog.auth_log(); 73 | assert_contains!(auth_log, "useradd"); 74 | } 75 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use core::fmt; 4 | 5 | #[macro_use] 6 | mod macros; 7 | 8 | mod helpers; 9 | mod su; 10 | mod sudo; 11 | mod visudo; 12 | 13 | type Error = Box; 14 | type Result = core::result::Result; 15 | 16 | const OTHER_USERNAME: &str = "ghost"; 17 | const USERNAME: &str = "ferris"; 18 | const GROUPNAME: &str = "rustaceans"; 19 | const PASSWORD: &str = "strong-password"; 20 | const HOSTNAME: &str = "container"; 21 | // 64 characters 22 | const LONGEST_HOSTNAME: &str = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"; 23 | 24 | const SUDOERS_ROOT_ALL: &str = "root ALL=(ALL:ALL) ALL"; 25 | const SUDOERS_ALL_ALL_NOPASSWD: &str = "ALL ALL=(ALL:ALL) NOPASSWD: ALL"; 26 | const SUDOERS_ROOT_ALL_NOPASSWD: &str = "root ALL=(ALL:ALL) NOPASSWD: ALL"; 27 | const SUDOERS_USER_ALL_NOPASSWD: &str = "ferris ALL=(ALL:ALL) NOPASSWD: ALL"; 28 | const SUDOERS_USER_ALL_ALL: &str = "ferris ALL=(ALL:ALL) ALL"; 29 | const SUDOERS_NO_LECTURE: &str = "Defaults lecture=\"never\""; 30 | const SUDOERS_ONCE_LECTURE: &str = "Defaults lecture=\"once\""; 31 | const SUDOERS_ALWAYS_LECTURE: &str = "Defaults lecture=\"always\""; 32 | const SUDOERS_NEW_LECTURE: &str = "Defaults lecture_file = \"/etc/sudo_lecture\""; 33 | const SUDOERS_NEW_LECTURE_USER: &str = "Defaults:ferris lecture_file = \"/etc/sudo_lecture\""; 34 | const PAMD_SUDO_PAM_PERMIT: &str = "auth sufficient pam_permit.so"; 35 | 36 | const OG_SUDO_STANDARD_LECTURE: &str= "\nWe trust you have received the usual lecture from the local System\nAdministrator. It usually boils down to these three things:\n\n #1) Respect the privacy of others.\n #2) Think before you type.\n #3) With great power comes great responsibility."; 37 | 38 | const SUDO_RS_IS_UNSTABLE: &str = 39 | "SUDO_RS_IS_UNSTABLE=I accept that my system may break unexpectedly"; 40 | 41 | const SUDO_ENV_DEFAULT_PATH: &str = "/usr/bin:/bin:/usr/sbin:/sbin"; 42 | const SUDO_ENV_DEFAULT_TERM: &str = "unknown"; 43 | 44 | const SUDOERS_USE_PTY: &str = "Defaults use_pty"; 45 | const SUDOERS_NOT_USE_PTY: &str = "Defaults !use_pty"; 46 | 47 | const ENV_PATH: &str = "/usr/bin/env"; 48 | 49 | const PANIC_EXIT_CODE: i32 = 101; 50 | 51 | enum EnvList { 52 | Check, 53 | Keep, 54 | } 55 | 56 | impl fmt::Display for EnvList { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | let s = match self { 59 | EnvList::Check => "env_check", 60 | EnvList::Keep => "env_keep", 61 | }; 62 | f.write_str(s) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! assert_contains { 2 | ($haystack:expr, $needle:expr) => { 3 | let haystack = &$haystack; 4 | let needle = &$needle; 5 | 6 | assert!( 7 | haystack.contains(needle), 8 | "{haystack:?} did not contain {needle:?}" 9 | ) 10 | }; 11 | } 12 | 13 | macro_rules! assert_not_contains { 14 | ($haystack:expr, $needle:expr) => { 15 | let haystack = &$haystack; 16 | let needle = &$needle; 17 | 18 | assert!( 19 | !haystack.contains(needle), 20 | "{haystack:?} did contain {needle:?}" 21 | ) 22 | }; 23 | } 24 | 25 | macro_rules! assert_starts_with { 26 | ($haystack:expr, $needle:expr) => { 27 | let haystack = &$haystack; 28 | let needle = &$needle; 29 | 30 | assert!( 31 | haystack.starts_with(needle), 32 | "{haystack:?} did not start with {needle:?}" 33 | ) 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_group/unassigned_group_id_is_rejected-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_group.rs 3 | expression: stderr 4 | --- 5 | sudo: unknown group #1234 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_group/unassigned_group_id_is_rejected.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_group.rs 3 | expression: stderr 4 | --- 5 | sudo: unknown group #1234 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_login/if_home_directory_does_not_exist_executes_program_without_changing_the_working_directory-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_login.rs 3 | expression: stderr 4 | --- 5 | sudo: unable to change directory to /home/ferris: No such file or directory 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_login/if_home_directory_does_not_exist_executes_program_without_changing_the_working_directory.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_login.rs 3 | expression: stderr 4 | --- 5 | sudo: unable to change directory to /home/ferris: No such file or directory 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_login/insufficient_permissions_to_execute_shell.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_login.rs 3 | expression: stderr 4 | --- 5 | sudo: unable to execute /tmp/my-shell: Permission denied 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_login/shell_does_not_exist.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_login.rs 3 | expression: stderr 4 | --- 5 | sudo: /tmp/my-shell: command not found 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_shell/shell_does_not_exist.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_shell.rs 3 | expression: stderr 4 | --- 5 | sudo: /root/my-shell: command not found 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_shell/shell_is_not_executable.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_shell.rs 3 | expression: stderr 4 | --- 5 | sudo: /root/my-shell: command not found 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_user/unassigned_user_id_is_rejected-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_user.rs 3 | expression: stderr 4 | --- 5 | sudo: unknown user #1234 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/flag_user/unassigned_user_id_is_rejected.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_user.rs 3 | expression: stderr 4 | --- 5 | sudo: unknown user #1234 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/misc/user_not_in_passwd_database_cannot_use_sudo.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/misc.rs 3 | expression: stderr 4 | --- 5 | sudo: you do not exist in the passwd database 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/passwd/explicit_passwd_overrides_nopasswd.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/passwd.rs 3 | expression: stderr 4 | --- 5 | [sudo] password for ferris: 6 | sudo: no password was provided 7 | sudo: a password is required 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/path_search/when_path_is_unset_does_not_search_in_default_path_set_for_command_execution.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/path_search.rs 3 | expression: stderr 4 | --- 5 | sudo: my-script: command not found 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/secure_path/dash_dash_before_flag_is_an_error.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/cli.rs 3 | expression: stderr 4 | --- 5 | sudo: -u: command not found 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd/command_specified_not_by_absolute_path_is_rejected.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd.rs 3 | expression: stderr 4 | --- 5 | /etc/sudoers:2:19: expected a fully-qualified path name 6 | ALL ALL=(ALL:ALL) true 7 | ^~~~ 8 | root is not in the sudoers file. 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd/given_specific_command_then_other_command_is_not_allowed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/another_negation_combination.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/combined_cmnd_aliases.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/command_alias_negation.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/command_specified_not_by_absolute_path_is_rejected.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | /etc/sudoers:2:24: expected a fully-qualified path name 6 | Cmnd_Alias CMDSGROUP = true, /usr/bin/ls 7 | ^~~~ 8 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/negation_not_order_sensitive.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/ls' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/one_more_negation_combination.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/runas_override-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/runas_override.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/ls' as ferris on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/tripple_negation_combination-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/ls' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/tripple_negation_combination.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/cmnd_alias/unlisted_cmnd_fails.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/cmnd_alias.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/host_alias/combined_host_aliases.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/host_alias.rs 3 | expression: stderr 4 | --- 5 | root is not allowed to run sudo on mail. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/host_alias/host_alias_negation.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/host_alias.rs 3 | expression: stderr 4 | --- 5 | root is not allowed to run sudo on mail. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/host_alias/negation_not_order_sensitive.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/host_alias.rs 3 | expression: stderr 4 | --- 5 | root is not allowed to run sudo on mail. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/host_alias/unlisted_host_fails.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/host_alias.rs 3 | expression: stderr 4 | --- 5 | root is not allowed to run sudo on not_listed. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/host_list/given_specific_hostname_then_sudo_from_different_hostname_is_rejected.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/host_list.rs 3 | expression: stderr 4 | --- 5 | root is not allowed to run sudo on container. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/host_list/negation_rejects.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/host_list.rs 3 | expression: stderr 4 | --- 5 | root is not allowed to run sudo on container. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/run_as/supplemental_group_matching.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/run_as.rs 3 | expression: stderr 4 | --- 5 | sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper 6 | sudo: a password is required 7 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/run_as/when_empty_then_as_someone_else_is_not_allowed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/run_as.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as ferris on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/run_as/when_only_group_is_specified_then_as_some_user_is_not_allowed-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/run_as.rs 3 | expression: stderr 4 | --- 5 | Sorry, user ferris is not allowed to execute '/usr/bin/true' as ghost on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/run_as/when_only_group_is_specified_then_as_some_user_is_not_allowed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/run_as.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as ghost on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/run_as/when_specific_group_then_as_a_different_group_is_not_allowed-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/run_as.rs 3 | expression: stderr 4 | --- 5 | Sorry, user ferris is not allowed to execute '/usr/bin/true' as ferris:ghosts on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/run_as/when_specific_group_then_as_a_different_group_is_not_allowed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/run_as.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root:ghosts on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/run_as/when_specific_user_then_as_a_different_user_is_not_allowed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/run_as.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as ghost on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/run_as/when_specific_user_then_as_self_is_not_allowed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/run_as.rs 3 | expression: stderr 4 | --- 5 | Sorry, user root is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/runas_alias/negation_on_user.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/runas_alias.rs 3 | expression: stderr 4 | --- 5 | [sudo] password for ferris: Sorry, user ferris is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/runas_alias/runas_alias_negation.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/runas_alias.rs 3 | expression: stderr 4 | --- 5 | [sudo] password for ferris: Sorry, user ferris is not allowed to execute '/usr/bin/true' as root on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/runas_alias/when_only_groupname_is_given_user_arg_fails.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/runas_alias.rs 3 | expression: stderr 4 | --- 5 | [sudo] password for ferris: Sorry, user ferris is not allowed to execute '/usr/bin/true' as otheruser on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/runas_alias/when_only_username_is_given_group_arg_fails.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/runas_alias.rs 3 | expression: stderr 4 | --- 5 | [sudo] password for ferris: Sorry, user ferris is not allowed to execute '/usr/bin/true' as ferris:rustaceans on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/runas_alias/when_specific_user_then_as_a_different_user_is_not_allowed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/runas_alias.rs 3 | expression: stderr 4 | --- 5 | [sudo] password for ferris: Sorry, user ferris is not allowed to execute '/usr/bin/true' as ghost on [host]. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/secure_path/if_set_it_does_not_search_in_original_user_path.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/secure_path.rs 3 | expression: stderr 4 | --- 5 | sudo: true: command not found 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/user_list/negated_subgroup.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/user_list.rs 3 | expression: output.stderr() 4 | --- 5 | ferris is not in the sudoers file. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/user_list/negated_supergroup-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/user_list.rs 3 | expression: stderr 4 | --- 5 | ghost is not in the sudoers file. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/user_list/negated_supergroup.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/user_list.rs 3 | expression: stderr 4 | --- 5 | ferris is not in the sudoers file. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/user_list/negation_excludes_group_members.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/user_list.rs 3 | expression: stderr 4 | --- 5 | ghost is not in the sudoers file. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/user_list/no_match.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/user_list.rs 3 | expression: stderr 4 | --- 5 | root is not in the sudoers file. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/sudoers/user_list/user_alias_works.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/sudoers/user_list.rs 3 | expression: stderr 4 | --- 5 | ghost is not in the sudoers file. 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/visudo/flag_file/passes_temporary_file_to_editor.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/visudo/flag_file.rs 3 | expression: args 4 | --- 5 | -- /tmp/[mkdtemp]/sudoers 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/visudo/passes_temporary_file_to_editor.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/visudo.rs 3 | expression: args 4 | --- 5 | -- /tmp/[mkdtemp]/sudoers 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/visudo/stderr_message_when_file_is_not_modified.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/visudo.rs 3 | expression: stderr 4 | --- 5 | visudo: /tmp/[mkdtemp]/sudoers unchanged 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/snapshots/visudo/temporary_file_is_deleted_during_edition.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/visudo.rs 3 | expression: "stderr.replace(ETC_DIR, \"\")" 4 | --- 5 | visudo: unable to re-open temporary file (/tmp/[mkdtemp]/sudoers), /sudoers unchanged: No such file or directory (os error 2) 6 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/su/cli.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, TextFile}; 2 | 3 | use crate::USERNAME; 4 | 5 | #[test] 6 | fn arguments_are_passed_to_shell() { 7 | let shell_path = "/tmp/my-shell"; 8 | let shell = r#"#!/bin/sh 9 | echo $0; echo $1; echo $2"#; 10 | let env = Env("") 11 | .user(USERNAME) 12 | .file(shell_path, TextFile(shell).chmod("755")) 13 | .build(); 14 | 15 | let shell_args = ["a", "b c"]; 16 | let stdout = Command::new("env") 17 | .args(["su", USERNAME, shell_path]) 18 | .args(shell_args) 19 | .output(&env) 20 | .stdout(); 21 | 22 | let [arg0, arg1] = shell_args; 23 | assert_eq!( 24 | format!( 25 | "{shell_path} 26 | {arg0} 27 | {arg1}" 28 | ), 29 | stdout 30 | ); 31 | } 32 | 33 | #[test] 34 | fn dash_user_shell_arguments() { 35 | let shell_path = "/tmp/my-shell"; 36 | let shell = r#"#!/bin/sh 37 | echo "${@}""#; 38 | let env = Env("") 39 | .user(USERNAME) 40 | .file(shell_path, TextFile(shell).chmod("755")) 41 | .build(); 42 | 43 | let shell_args = ["a", "b c"]; 44 | let stdout = Command::new("env") 45 | .args(["su", "-", USERNAME, shell_path]) 46 | .args(shell_args) 47 | .output(&env) 48 | .stdout(); 49 | 50 | assert_eq!(shell_args.join(" "), stdout); 51 | } 52 | 53 | #[test] 54 | fn flag_after_positional_argument() { 55 | let expected = "-sh"; 56 | let env = Env("").build(); 57 | let stdout = Command::new("env") 58 | .args(["su", "-c", "echo $0", "root", "-l"]) 59 | .output(&env) 60 | .stdout(); 61 | 62 | assert_eq!(expected, stdout); 63 | } 64 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/su/flag_command.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, TextFile}; 2 | 3 | #[test] 4 | fn it_works() { 5 | let env = Env("").build(); 6 | 7 | Command::new("su") 8 | .args(["-c", "true"]) 9 | .output(&env) 10 | .assert_success(); 11 | 12 | let output = Command::new("su").args(["-c", "false"]).output(&env); 13 | 14 | output.assert_exit_code(1); 15 | } 16 | 17 | #[test] 18 | fn pass_to_shell_via_c_flag() { 19 | let shell_path = "/root/my-shell"; 20 | let my_shell = "#!/bin/sh 21 | echo $@"; 22 | let env = Env("") 23 | .file(shell_path, TextFile(my_shell).chmod("100")) 24 | .build(); 25 | 26 | let command = "command"; 27 | let output = Command::new("su") 28 | .args(["-s", shell_path, "-c", command]) 29 | .output(&env) 30 | .stdout(); 31 | 32 | assert_eq!(format!("-c {command}"), output); 33 | } 34 | 35 | #[test] 36 | fn when_specified_more_than_once_only_last_value_is_used() { 37 | let env = Env("").build(); 38 | 39 | let output = Command::new("su") 40 | .args(["-c", "id"]) 41 | .args(["-c", "true"]) 42 | .output(&env); 43 | 44 | output.assert_success(); 45 | assert!(output.stderr().is_empty()); 46 | assert!(output.stdout().is_empty()); 47 | } 48 | 49 | #[test] 50 | fn positional_arguments_are_not_passed_to_command() { 51 | let env = Env("").build(); 52 | 53 | let argss = [["-c", "echo", "root", "a"], ["root", "-c", "echo", "a"]]; 54 | 55 | for args in argss { 56 | let output = Command::new("su").args(args).output(&env); 57 | let stdout = output.stdout(); 58 | 59 | assert!(stdout.trim().is_empty()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/su/flag_group.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, Group}; 2 | 3 | use crate::Result; 4 | 5 | #[test] 6 | fn sets_primary_group_id() -> Result<()> { 7 | let gid = 1000; 8 | let group_name = "rustaceans"; 9 | let env = Env("").group(Group(group_name).id(gid)).build(); 10 | 11 | let actual = Command::new("su") 12 | .args(["-g", group_name, "-c", "id -g"]) 13 | .output(&env) 14 | .stdout() 15 | .parse::()?; 16 | 17 | assert_eq!(gid, actual); 18 | 19 | Ok(()) 20 | } 21 | 22 | #[test] 23 | fn original_primary_group_id_is_lost() { 24 | let gid = 1000; 25 | let group_name = "rustaceans"; 26 | let env = Env("").group(Group(group_name).id(gid)).build(); 27 | 28 | let actual = Command::new("su") 29 | .args(["-g", group_name, "-c", "id -G"]) 30 | .output(&env) 31 | .stdout(); 32 | 33 | assert_eq!(gid.to_string(), actual); 34 | } 35 | 36 | #[test] 37 | fn invoking_user_must_be_root() { 38 | let group_name = "rustaceans"; 39 | let invoking_user = "ferris"; 40 | let a_target_user = "ghost"; 41 | let env = Env("") 42 | .user(invoking_user) 43 | .user(a_target_user) 44 | .group(group_name) 45 | .build(); 46 | 47 | let target_users = ["root", a_target_user]; 48 | 49 | for target_user in target_users { 50 | let output = Command::new("su") 51 | .args(["-g", group_name, target_user]) 52 | .as_user(invoking_user) 53 | .output(&env); 54 | 55 | output.assert_exit_code(1); 56 | assert_contains!( 57 | output.stderr(), 58 | "su: only root can specify alternative groups" 59 | ); 60 | } 61 | } 62 | 63 | #[test] 64 | fn when_specified_more_than_once_all_groups_are_added_to_group_list() { 65 | let gid1 = 1000; 66 | let group_name1 = "rustaceans"; 67 | let gid2 = 1001; 68 | let group_name2 = "crabs"; 69 | let env = Env("") 70 | .group(Group(group_name1).id(gid1)) 71 | .group(Group(group_name2).id(gid2)) 72 | .build(); 73 | 74 | let actual = Command::new("su") 75 | .args(["-g", group_name1, "-g", group_name2, "-c", "id -G"]) 76 | .output(&env) 77 | .stdout(); 78 | 79 | assert_eq!(format!("{gid2} {gid1}"), actual); 80 | } 81 | 82 | #[test] 83 | fn last_group_argument_becomes_primary_group() -> Result<()> { 84 | let gid1 = 1000; 85 | let group_name1 = "rustaceans"; 86 | let gid2 = 1001; 87 | let group_name2 = "crabs"; 88 | let env = Env("") 89 | .group(Group(group_name1).id(gid1)) 90 | .group(Group(group_name2).id(gid2)) 91 | .build(); 92 | 93 | let actual = Command::new("su") 94 | .args(["-g", group_name1, "-g", group_name2, "-c", "id -g"]) 95 | .output(&env) 96 | .stdout() 97 | .parse::()?; 98 | 99 | assert_eq!(gid2, actual); 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/su/limits.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, User}; 2 | 3 | use crate::{PASSWORD, USERNAME}; 4 | 5 | #[test] 6 | #[cfg_attr( 7 | target_os = "freebsd", 8 | ignore = "FreeBSD doesn't support /etc/security" 9 | )] 10 | fn etc_security_limits_rules_apply_according_to_the_target_user() { 11 | let target_user = "ghost"; 12 | let original = "2048"; 13 | let expected = "1024"; 14 | let limits = format!( 15 | "{USERNAME} hard locks {original} 16 | {target_user} hard locks {expected}" 17 | ); 18 | let env = Env("") 19 | .file("/etc/security/limits.d/50-test.conf", limits) 20 | .user(USERNAME) 21 | .user(User(target_user).password(PASSWORD).shell("/bin/bash")) 22 | .build(); 23 | 24 | // this appears to ignore the `limits` rules, perhaps because of docker 25 | // in any case, the assertion below and the rule above should be enough to check that the 26 | // *target* user's, and not the invoking user's, limits apply when su is involved 27 | // let normal_limit = Command::new("bash") 28 | // .args(["-c", "ulimit -x"]) 29 | // .as_user(USERNAME) 30 | // .output(&env) 31 | // .stdout(); 32 | 33 | // assert_eq!(original, normal_limit); 34 | 35 | // check that limits apply even when root is the invoking user 36 | let users = ["root", USERNAME]; 37 | for invoking_user in users { 38 | let su_limit = Command::new("su") 39 | .args(["-c", "ulimit -x", target_user]) 40 | .stdin(PASSWORD) 41 | .as_user(invoking_user) 42 | .output(&env) 43 | .stdout(); 44 | 45 | assert_eq!(expected, su_limit); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/su/pam.rs: -------------------------------------------------------------------------------- 1 | //! PAM integration tests 2 | 3 | use sudo_test::{Command, Env, User, BIN_TRUE}; 4 | 5 | use crate::{PASSWORD, USERNAME}; 6 | 7 | #[test] 8 | fn given_pam_permit_then_no_password_auth_required() { 9 | let env = Env("") 10 | .user(USERNAME) 11 | .file("/etc/pam.d/su", "auth sufficient pam_permit.so") 12 | .build(); 13 | 14 | Command::new("su") 15 | .args(["-c", BIN_TRUE]) 16 | .as_user(USERNAME) 17 | .output(&env) 18 | .assert_success(); 19 | } 20 | 21 | #[test] 22 | fn given_pam_deny_then_password_auth_always_fails() { 23 | let invoking_user = USERNAME; 24 | let target_user = "ghost"; 25 | 26 | let env = Env("") 27 | .file("/etc/pam.d/su", "auth requisite pam_deny.so") 28 | .user(invoking_user) 29 | .user(User(target_user).password(PASSWORD)) 30 | .build(); 31 | 32 | let output = Command::new("su") 33 | .args(["-s", BIN_TRUE, target_user]) 34 | .as_user(invoking_user) 35 | .stdin(PASSWORD) 36 | .output(&env); 37 | 38 | output.assert_exit_code(1); 39 | 40 | let diagnostic = if sudo_test::is_original_sudo() { 41 | "su: Authentication failure" 42 | } else { 43 | "3 incorrect authentication attempts" 44 | }; 45 | assert_contains!(output.stderr(), diagnostic); 46 | } 47 | 48 | #[test] 49 | fn being_root_has_precedence_over_missing_pam_file() { 50 | let env = Env("").build(); 51 | 52 | Command::new("su").output(&env).assert_success(); 53 | } 54 | 55 | #[test] 56 | fn being_root_has_no_precedence_over_pam_deny() { 57 | let env = Env("") 58 | .file("/etc/pam.d/su", "auth requisite pam_deny.so") 59 | .build(); 60 | 61 | let output = Command::new("su").args(["-c", BIN_TRUE]).output(&env); 62 | 63 | output.assert_exit_code(1); 64 | 65 | let diagnostic = if sudo_test::is_original_sudo() { 66 | "su: Authentication failure" 67 | } else { 68 | "3 incorrect authentication attempts" 69 | }; 70 | assert_contains!(output.stderr(), diagnostic); 71 | } 72 | 73 | #[test] 74 | #[cfg_attr(target_os = "freebsd", ignore = "FreeBSD doesn't use /etc/pam.d/su-l")] 75 | fn su_uses_correct_service_file() { 76 | let env = Env("") 77 | .file("/etc/pam.d/su", "auth sufficient pam_permit.so") 78 | .file("/etc/pam.d/su-l", "auth requisite pam_deny.so") 79 | .user(USERNAME) 80 | .build(); 81 | 82 | Command::new("su") 83 | .args(["-c", "true"]) 84 | .as_user(USERNAME) 85 | .output(&env) 86 | .assert_success(); 87 | } 88 | 89 | #[test] 90 | #[cfg_attr(target_os = "freebsd", ignore = "FreeBSD doesn't use /etc/pam.d/su-l")] 91 | fn su_dash_l_uses_correct_service_file() { 92 | let env = Env("") 93 | .file("/etc/pam.d/su-l", "auth sufficient pam_permit.so") 94 | .file("/etc/pam.d/su", "auth requisite pam_deny.so") 95 | .user(USERNAME) 96 | .build(); 97 | 98 | Command::new("su") 99 | .args(["-l", "-c", "true"]) 100 | .as_user(USERNAME) 101 | .output(&env) 102 | .assert_success(); 103 | } 104 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "apparmor")] 2 | mod apparmor; 3 | mod child_process; 4 | mod cli; 5 | mod env_reset; 6 | mod flag_chdir; 7 | mod flag_group; 8 | mod flag_help; 9 | mod flag_list; 10 | mod flag_login; 11 | mod flag_non_interactive; 12 | mod flag_prompt; 13 | mod flag_shell; 14 | mod flag_user; 15 | mod flag_version; 16 | mod lecture; 17 | mod lecture_file; 18 | mod limits; 19 | mod misc; 20 | mod nopasswd; 21 | mod pam; 22 | mod pass_auth; 23 | mod passwd; 24 | mod password_retry; 25 | mod path_search; 26 | mod perms; 27 | mod sudo_ps1; 28 | mod sudoers; 29 | mod syslog; 30 | mod timestamp; 31 | mod use_pty; 32 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/apparmor.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env}; 2 | 3 | use crate::Result; 4 | 5 | #[test] 6 | fn can_switch_the_apparmor_profile() -> Result<()> { 7 | let env = Env("root ALL=(ALL:ALL) APPARMOR_PROFILE=docker-default ALL") 8 | .apparmor("unconfined") 9 | .build(); 10 | 11 | let output = Command::new("sudo") 12 | .args(["-s", "cat", "/proc/$$/attr/current"]) 13 | .output(&env); 14 | dbg!(&output); 15 | 16 | output.assert_success(); 17 | assert_eq!(output.stdout(), "docker-default (enforce)"); 18 | 19 | Ok(()) 20 | } 21 | 22 | #[test] 23 | fn cannot_switch_to_nonexisting_profile() -> Result<()> { 24 | let env = Env("root ALL=(ALL:ALL) APPARMOR_PROFILE=this_profile_does_not_exist ALL").build(); 25 | 26 | let output = Command::new("sudo").arg("true").output(&env); 27 | 28 | dbg!(&output); 29 | 30 | output.assert_exit_code(1); 31 | assert_contains!(output.stderr(), "unable to change AppArmor profile"); 32 | 33 | Ok(()) 34 | } 35 | 36 | #[test] 37 | fn can_set_default_apparmor_profile() -> Result<()> { 38 | let env = Env("root ALL=(ALL:ALL) ALL 39 | Defaults apparmor_profile=docker-default 40 | ") 41 | .apparmor("unconfined") 42 | .build(); 43 | 44 | let output = Command::new("sudo") 45 | .args(["-s", "cat", "/proc/$$/attr/current"]) 46 | .output(&env); 47 | dbg!(&output); 48 | 49 | output.assert_success(); 50 | assert_eq!(output.stdout(), "docker-default (enforce)"); 51 | 52 | Ok(()) 53 | } 54 | 55 | #[test] 56 | fn tags_override_the_default_apparmor_profile() -> Result<()> { 57 | let env = Env("root ALL=(ALL:ALL) APPARMOR_PROFILE=unconfined ALL 58 | Defaults apparmor_profile=docker-default 59 | ") 60 | .apparmor("unconfined") 61 | .build(); 62 | 63 | let output = Command::new("sudo") 64 | .args(["-s", "cat", "/proc/$$/attr/current"]) 65 | .output(&env); 66 | dbg!(&output); 67 | 68 | output.assert_success(); 69 | assert_eq!(output.stdout(), "unconfined"); 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/child_process.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | use sudo_test::{Command, Env}; 3 | 4 | use crate::SUDOERS_ROOT_ALL_NOPASSWD; 5 | 6 | mod signal_handling; 7 | 8 | #[test] 9 | fn sudo_forwards_childs_exit_code() { 10 | let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build(); 11 | 12 | let expected = 42; 13 | let output = Command::new("sudo") 14 | .args(["sh", "-c"]) 15 | .arg(format!("exit {expected}")) 16 | .output(&env); 17 | output.assert_exit_code(expected); 18 | } 19 | 20 | #[test] 21 | fn sudo_forwards_childs_stdout() { 22 | let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build(); 23 | 24 | let expected = "hello"; 25 | let output = Command::new("sudo").args(["echo", expected]).output(&env); 26 | assert!(output.stderr().is_empty()); 27 | assert_eq!(expected, output.stdout()); 28 | } 29 | 30 | #[test] 31 | fn sudo_forwards_childs_stderr() { 32 | let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build(); 33 | 34 | let expected = "hello"; 35 | let output = Command::new("sudo") 36 | .args(["sh", "-c"]) 37 | .arg(format!(">&2 echo {expected}")) 38 | .output(&env); 39 | assert_eq!(expected, output.stderr()); 40 | assert!(output.stdout().is_empty()); 41 | } 42 | 43 | #[test] 44 | fn sudo_forwards_stdin_to_child() { 45 | let expected = "hello"; 46 | let path = "/root/file"; 47 | let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build(); 48 | 49 | Command::new("sudo") 50 | .args(["tee", path]) 51 | .stdin(expected) 52 | .output(&env) 53 | .assert_success(); 54 | 55 | let actual = Command::new("cat").arg(path).output(&env).stdout(); 56 | 57 | assert_eq!(expected, actual); 58 | } 59 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/child_process/signal_handling/change-size.sh: -------------------------------------------------------------------------------- 1 | # Wait for `print-sizes.sh` to write to `/tmp/tty_path` and report the old tty size. 2 | until [ -f /tmp/barrier1 ]; do sleep 0.1; done 3 | # Resize the terminal 4 | stty -F$(cat /tmp/tty_path) rows 42 cols 69 5 | # Notify `print-sizes.sh` that the tty size has changed. 6 | touch /tmp/barrier2 7 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/child_process/signal_handling/expects-signal.sh: -------------------------------------------------------------------------------- 1 | trap 'echo got signal && exit 0' $@ 2 | for _ in $(seq 1 20); do 3 | sleep 0.1 4 | done 5 | echo >&2 received no signal 6 | exit 1 7 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/child_process/signal_handling/kill-sudo-parent.sh: -------------------------------------------------------------------------------- 1 | trap 'echo received SIGTERM; exit 1' TERM 2 | sudo_pid="$(pidof sudo)" 3 | [ -n "$sudo_pid" ] || (echo sudo process not found && exit 2) 4 | kill "$sudo_pid" 5 | # as insurance, wait a bit for signal delivery to happen 6 | # the signal handler won't run while `sleep` runs hence the multiple `sleep` invocations 7 | for _ in $(seq 1 10); do 8 | sleep 0.1 9 | done 10 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/child_process/signal_handling/kill-sudo.sh: -------------------------------------------------------------------------------- 1 | # because the sudo process is `spawn`-ed it may not be immediately visible so 2 | # retry `pidof` until it becomes visible 3 | for _ in $(seq 1 20); do 4 | # when sudo runs with `use_pty` there are two sudo processes as sudo spawns 5 | # a monitor process. We want the PID of the sudo process so we assume it 6 | # must be the smallest of the returned PIDs. 7 | sudopid=$(pidof sudo | sort -gr | cut -f 1 -d ' ') 8 | 9 | if [ -n "$sudopid" ]; then 10 | # give `expects-signal.sh ` some time to execute the `trap` command 11 | # otherwise it'll be terminated before the signal handler is installed 12 | sleep 0.1 13 | kill $1 "$sudopid" 14 | exit 0 15 | fi 16 | sleep 0.1 17 | done 18 | 19 | echo >&2 timeout 20 | exit 1 21 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/child_process/signal_handling/print-sizes.sh: -------------------------------------------------------------------------------- 1 | # Save the name of the current tty so `change-size.sh` can read it. 2 | tty > /tmp/tty_path 3 | # Print the current terminal size 4 | stty size 5 | # Print the terminal size, notify `change-size.sh` that it can change the tty size, wait for it to 6 | # finish and then print the terminal size again. 7 | sudo sh -c "stty size; touch /tmp/barrier1; until [ -f /tmp/barrier2 ]; do sleep 0.1; done; stty size" 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/child_process/signal_handling/sigtstp.bash: -------------------------------------------------------------------------------- 1 | set -e 2 | # enable 'job control' to make `fg` work 3 | set -m 4 | 5 | sudo sh -c 'for i in $(seq 1 5); do date +%s; sleep 1; done' & 6 | sleep 2 7 | kill -TSTP $! 8 | sleep 5 9 | fg 10 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_help.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env}; 2 | 3 | use crate::{Result, PANIC_EXIT_CODE}; 4 | 5 | #[test] 6 | fn does_not_panic_on_io_errors() -> Result<()> { 7 | let env = Env("").build(); 8 | 9 | let output = Command::new("bash") 10 | .args(["-c", "sudo --help 2>&1 | true; echo \"${PIPESTATUS[0]}\""]) 11 | .output(&env); 12 | 13 | let exit_code = output.stdout().parse()?; 14 | assert_ne!(PANIC_EXIT_CODE, exit_code); 15 | assert_eq!(0, exit_code); 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/credential_caching.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, User}; 2 | 3 | use crate::{PASSWORD, USERNAME}; 4 | 5 | #[test] 6 | fn it_works() { 7 | let hostname = "container"; 8 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) ALL")) 9 | .user(User(USERNAME).password(PASSWORD)) 10 | .hostname(hostname) 11 | .build(); 12 | 13 | let output = Command::new("sh") 14 | .arg("-c") 15 | .arg(format!("echo {PASSWORD} | sudo -S -l; sudo -l && true")) 16 | .as_user(USERNAME) 17 | .output(&env); 18 | 19 | let stdout = output.stdout(); 20 | let it_worked = stdout 21 | .lines() 22 | .filter(|line| { 23 | line.starts_with(&format!( 24 | "User {USERNAME} may run the following commands on {hostname}:" 25 | )) 26 | }) 27 | .count(); 28 | 29 | assert_eq!(2, it_worked); 30 | } 31 | 32 | #[test] 33 | fn credential_shared_with_non_list_sudo() { 34 | let hostname = "container"; 35 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) ALL")) 36 | .user(User(USERNAME).password(PASSWORD)) 37 | .hostname(hostname) 38 | .build(); 39 | 40 | Command::new("sh") 41 | .arg("-c") 42 | .arg(format!( 43 | "echo {PASSWORD} | sudo -S -l 2>/dev/null >/tmp/stdout1.txt; sudo true && true" 44 | )) 45 | .as_user(USERNAME) 46 | .output(&env) 47 | .assert_success(); 48 | 49 | let stdout1 = Command::new("cat") 50 | .arg("/tmp/stdout1.txt") 51 | .output(&env) 52 | .stdout(); 53 | 54 | assert_contains!( 55 | stdout1, 56 | format!("User {USERNAME} may run the following commands on {hostname}:") 57 | ); 58 | } 59 | 60 | #[test] 61 | fn flag_reset_timestamp() { 62 | let hostname = "container"; 63 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) ALL")) 64 | .user(User(USERNAME).password(PASSWORD)) 65 | .hostname(hostname) 66 | .build(); 67 | 68 | let output = Command::new("sh") 69 | .arg("-c") 70 | .arg(format!( 71 | "echo {PASSWORD} | sudo -S -l 2>/dev/null >/tmp/stdout1.txt; sudo -k; sudo -l" 72 | )) 73 | .as_user(USERNAME) 74 | .output(&env); 75 | 76 | let stdout1 = Command::new("cat") 77 | .arg("/tmp/stdout1.txt") 78 | .output(&env) 79 | .stdout(); 80 | 81 | assert_contains!( 82 | stdout1, 83 | format!("User {USERNAME} may run the following commands on {hostname}:") 84 | ); 85 | 86 | assert!(!output.status().success()); 87 | let diagnostic = if sudo_test::is_original_sudo() { 88 | "sudo: a password is required" 89 | } else { 90 | "sudo: Authentication failed" 91 | }; 92 | assert_contains!(output.stderr(), diagnostic); 93 | } 94 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/flag_other_user.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, User}; 2 | 3 | use crate::{PASSWORD, USERNAME}; 4 | 5 | #[test] 6 | fn other_user_does_not_exist() { 7 | let env = Env("").build(); 8 | 9 | let output = Command::new("sudo") 10 | .args(["-l", "-U", USERNAME]) 11 | .output(&env); 12 | 13 | eprintln!("{}", output.stderr()); 14 | 15 | output.assert_exit_code(1); 16 | let diagnostic = if sudo_test::is_original_sudo() { 17 | format!("sudo: unknown user {USERNAME}") 18 | } else { 19 | format!("sudo-rs: user '{USERNAME}' not found") 20 | }; 21 | assert_contains!(output.stderr(), diagnostic); 22 | } 23 | 24 | #[test] 25 | fn other_user_is_self() { 26 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) /bin/ls")) 27 | .user(User(USERNAME).password(PASSWORD)) 28 | .build(); 29 | 30 | let output = Command::new("sudo") 31 | .args(["-S", "-l", "-U", USERNAME]) 32 | .as_user(USERNAME) 33 | .stdin(PASSWORD) 34 | .output(&env); 35 | 36 | output.assert_success(); 37 | } 38 | 39 | #[test] 40 | fn current_user_is_root() { 41 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) /bin/ls")) 42 | .user(USERNAME) 43 | .build(); 44 | 45 | let output = Command::new("sudo") 46 | .args(["-l", "-U", USERNAME]) 47 | .output(&env); 48 | 49 | output.assert_success(); 50 | } 51 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/command_alias.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | 17 | /usr/bin/true 18 | /usr/bin/false 19 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/command_arguments.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | /usr/bin/true a b c 17 | /usr/bin/false 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/complex_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: !ferris, root 15 | RunAsGroups: crabs, !root 16 | Commands: 17 | ALL 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Cwd: * 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: ferris 21 | Cwd: * 22 | Commands: 23 | /usr/bin/false 24 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Cwd: * 16 | Commands: 17 | /usr/bin/true 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_multiple_commands.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Cwd: * 16 | Commands: 17 | /usr/bin/true 18 | /usr/bin/false 19 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_multiple_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Cwd: * 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: ferris 21 | Cwd: * 22 | Commands: 23 | /usr/bin/false 24 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_nopasswd.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: !authenticate 16 | Cwd: * 17 | Commands: 18 | /usr/bin/true 19 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_not_in_first_position.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | /usr/bin/true 17 | 18 | Sudoers entry: 19 | RunAsUsers: root 20 | Cwd: * 21 | Commands: 22 | /usr/bin/false 23 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_override.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Cwd: * 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: root 21 | Cwd: /home 22 | Commands: 23 | /usr/bin/false 24 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_override_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Cwd: * 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: ferris 21 | Cwd: * 22 | Commands: 23 | /usr/bin/false 24 | 25 | Sudoers entry: 26 | RunAsUsers: ferris 27 | Cwd: /home 28 | Commands: 29 | 30 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/cwd_path.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Cwd: /home 16 | Commands: 17 | /usr/bin/true 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/empty_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: ferruccio 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: ferruccio 15 | RunAsGroups: crabs 16 | Commands: 17 | ALL 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/implicit_runas_group.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | /usr/bin/true 17 | 18 | Sudoers entry: 19 | RunAsUsers: ferris 20 | Commands: 21 | /usr/bin/false 22 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/multiple_commands.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | /usr/bin/true 17 | /usr/bin/false 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/multiple_group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: ferruccio 15 | RunAsGroups: crabs, root 16 | Commands: 17 | ALL 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/multiple_lines.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | /usr/bin/true 17 | /usr/bin/false 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/multiple_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | /usr/bin/true 17 | 18 | Sudoers entry: 19 | RunAsUsers: ferris 20 | Commands: 21 | /usr/bin/false 22 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/multiple_users_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: ferris, root 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/negated_command_alias.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | 17 | !/usr/bin/true 18 | /usr/bin/false 19 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/no_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/nopasswd.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: !authenticate 16 | Commands: 17 | /usr/bin/true 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/nopasswd_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: !authenticate 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: ferris 21 | Options: !authenticate 22 | Commands: 23 | /usr/bin/false 24 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/nopasswd_passwd_on_same_command.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: authenticate 16 | Commands: 17 | /usr/bin/true 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/nopasswd_passwd_override.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: !authenticate 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: root 21 | Options: authenticate 22 | Commands: 23 | /usr/bin/false 24 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/nopasswd_passwd_override_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: !authenticate 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: ferris 21 | Options: !authenticate 22 | Commands: 23 | /usr/bin/false 24 | 25 | Sudoers entry: 26 | RunAsUsers: ferris 27 | Options: authenticate 28 | Commands: 29 | 30 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/not_group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: ferruccio 15 | RunAsGroups: !crabs 16 | Commands: 17 | ALL 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/not_user_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: !ferris 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/passwd.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: authenticate 16 | Commands: 17 | /usr/bin/true 18 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/passwd_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: authenticate 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: ferris 21 | Options: authenticate 22 | Commands: 23 | /usr/bin/false 24 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/passwd_nopasswd_override.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: root 15 | Options: authenticate 16 | Commands: 17 | /usr/bin/true 18 | 19 | Sudoers entry: 20 | RunAsUsers: root 21 | Options: !authenticate 22 | Commands: 23 | /usr/bin/false 24 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/user_group_id_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: %#0 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/user_group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: %root 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/user_id_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: #0 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/user_non_unix_group_id_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: %:#0 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/user_non_unix_group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: %:root 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/long_format/snapshots/user_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/long_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | 7 | Sudoers entry: 8 | RunAsUsers: root 9 | Options: !authenticate 10 | Commands: 11 | /tmp 12 | 13 | Sudoers entry: 14 | RunAsUsers: ferris 15 | Commands: 16 | ALL 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/needs_auth.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env}; 2 | 3 | use crate::USERNAME; 4 | 5 | #[test] 6 | fn when_other_user_is_self() { 7 | let env = Env("Defaults !lecture 8 | ALL ALL=(ALL:ALL) ALL") 9 | .user(USERNAME) 10 | .build(); 11 | 12 | let output = Command::new("sudo") 13 | .args(["-S", "-l", "-U", USERNAME]) 14 | .as_user(USERNAME) 15 | .output(&env); 16 | 17 | output.assert_exit_code(1); 18 | 19 | let diagnostic = if sudo_test::is_original_sudo() { 20 | if cfg!(not(target_os = "linux")) { 21 | "Password:".to_owned() 22 | } else { 23 | format!("[sudo] password for {USERNAME}:") 24 | } 25 | } else { 26 | "[sudo: authenticate] Password:".to_string() 27 | }; 28 | assert_contains!(output.stderr(), diagnostic); 29 | } 30 | 31 | #[test] 32 | fn other_user_has_nopasswd_tag() { 33 | let other_user = "ghost"; 34 | let env = Env(format!( 35 | "Defaults !lecture 36 | {other_user} ALL=(ALL:ALL) NOPASSWD: ALL 37 | {USERNAME} ALL=(ALL:ALL) ALL" 38 | )) 39 | .user(USERNAME) 40 | .user(other_user) 41 | .build(); 42 | 43 | let output = Command::new("sudo") 44 | .args(["-S", "-l", "-U", other_user]) 45 | .as_user(USERNAME) 46 | .output(&env); 47 | 48 | output.assert_exit_code(1); 49 | 50 | let diagnostic = if sudo_test::is_original_sudo() { 51 | if cfg!(not(target_os = "linux")) { 52 | "Password:".to_owned() 53 | } else { 54 | format!("[sudo] password for {USERNAME}:") 55 | } 56 | } else { 57 | "[sudo: authenticate] Password:".to_string() 58 | }; 59 | assert_contains!(output.stderr(), diagnostic); 60 | } 61 | 62 | #[test] 63 | fn listpw_any_by_default() { 64 | let env = Env(format!( 65 | "Defaults !lecture 66 | {USERNAME} ALL=(ALL:ALL) NOPASSWD: /bin/ls 67 | {USERNAME} ALL=(ALL:ALL) /bin/ls" 68 | )) 69 | .user(USERNAME) 70 | .build(); 71 | 72 | let output = Command::new("sudo") 73 | .args(["-S", "-l"]) 74 | .as_user(USERNAME) 75 | .output(&env); 76 | 77 | output.assert_success(); 78 | } 79 | 80 | #[test] 81 | fn use_proper_last_matching_tag_for_other_user() { 82 | let other_user = "ghost"; 83 | let env = Env(format!( 84 | "Defaults !lecture 85 | {USERNAME} ALL=(ALL:ALL) PASSWD: ALL 86 | {USERNAME} ALL=(ALL:ALL) NOPASSWD: /bin/ls 87 | " 88 | )) 89 | .user(USERNAME) 90 | .user(other_user) 91 | .build(); 92 | 93 | let output = Command::new("sudo") 94 | .args(["-S", "-l", "-U", other_user]) 95 | .as_user(USERNAME) 96 | .output(&env); 97 | 98 | output.assert_exit_code(1); 99 | 100 | let diagnostic = if sudo_test::is_original_sudo() { 101 | if cfg!(not(target_os = "linux")) { 102 | "Password:".to_owned() 103 | } else { 104 | format!("[sudo] password for {USERNAME}:") 105 | } 106 | } else { 107 | "[sudo: authenticate] Password:".to_string() 108 | }; 109 | assert_contains!(output.stderr(), diagnostic); 110 | } 111 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/command_alias.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) , /usr/bin/true, /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/command_arguments.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) /usr/bin/true a b c, /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/complex_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (!ferris, root : crabs, !root) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) CWD=* /usr/bin/true 8 | (ferris) CWD=* /usr/bin/false 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) CWD=* /usr/bin/true 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_multiple_commands.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) CWD=* /usr/bin/true, /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_multiple_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) CWD=* /usr/bin/true 8 | (ferris) CWD=* /usr/bin/false 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_nopasswd.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) CWD=* NOPASSWD: /usr/bin/true 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_not_in_first_position.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) /usr/bin/true, CWD=* /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_override.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) CWD=* /usr/bin/true, CWD=/home /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_override_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) CWD=* /usr/bin/true 8 | (ferris) CWD=* /usr/bin/false, CWD=/home 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/cwd_path.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) CWD=/home /usr/bin/true 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/empty_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (ferruccio) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/empty_runas_with_colon.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (ferruccio) /usr/bin/true 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (ferruccio : crabs) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/implicit_runas_group.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) /usr/bin/true 8 | (ferris) /usr/bin/false 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/multiple_commands.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) /usr/bin/true, /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/multiple_group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (ferruccio : crabs, root) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/multiple_lines.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) /usr/bin/true, /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/multiple_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) /usr/bin/true 8 | (ferris) /usr/bin/false 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/multiple_users_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (ferris, root) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/negated_command_alias.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) , !/usr/bin/true, /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/no_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/nopasswd.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) NOPASSWD: /usr/bin/true 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/nopasswd_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) NOPASSWD: /usr/bin/true 8 | (ferris) NOPASSWD: /usr/bin/false 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/nopasswd_passwd_on_same_command.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) PASSWD: /usr/bin/true 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/nopasswd_passwd_override.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) NOPASSWD: /usr/bin/true, PASSWD: /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/nopasswd_passwd_override_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) NOPASSWD: /usr/bin/true 8 | (ferris) NOPASSWD: /usr/bin/false, PASSWD: 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/not_group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (ferruccio : !crabs) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/not_user_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (!ferris) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/passwd.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) PASSWD: /usr/bin/true 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/passwd_across_runas_groups.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) PASSWD: /usr/bin/true 8 | (ferris) PASSWD: /usr/bin/false 9 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/passwd_nopasswd_override.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (root) PASSWD: /usr/bin/true, NOPASSWD: /usr/bin/false 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/user_group_id_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (%#0) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/user_group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (%root) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/user_id_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (#0) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/user_non_unix_group_id_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (%:#0) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/user_non_unix_group_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (%:root) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/short_format/snapshots/user_runas.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: sudo-compliance-tests/src/sudo/flag_list/short_format/mod.rs 3 | expression: stdout 4 | --- 5 | User ferruccio may run the following commands on container: 6 | (root) NOPASSWD: /tmp 7 | (ferris) ALL 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_list/sudoers_list.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, User}; 2 | 3 | use crate::{HOSTNAME, OTHER_USERNAME, PASSWORD, USERNAME}; 4 | 5 | #[test] 6 | fn invoking_user_has_list_perms() { 7 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) list")) 8 | .user(User(USERNAME).password(PASSWORD)) 9 | .hostname(HOSTNAME) 10 | .build(); 11 | 12 | let output = Command::new("sudo") 13 | .args(["-S", "-l"]) 14 | .stdin(PASSWORD) 15 | .as_user(USERNAME) 16 | .output(&env); 17 | 18 | assert_contains!( 19 | output.stdout(), 20 | format!("User {USERNAME} may run the following commands on {HOSTNAME}:") 21 | ); 22 | } 23 | 24 | #[test] 25 | fn invoking_user_has_list_perms_nopasswd() { 26 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) NOPASSWD: list")) 27 | .user(USERNAME) 28 | .hostname(HOSTNAME) 29 | .build(); 30 | 31 | let output = Command::new("sudo") 32 | .arg("-l") 33 | .as_user(USERNAME) 34 | .output(&env); 35 | 36 | assert_contains!( 37 | output.stdout(), 38 | format!( 39 | "User {USERNAME} may run the following commands on {HOSTNAME}: 40 | (ALL : ALL) NOPASSWD: list" 41 | ) 42 | ); 43 | } 44 | 45 | #[test] 46 | fn other_user_has_list_perms_but_invoking_user_has_not() { 47 | let env = Env(format!("{OTHER_USERNAME} ALL=(ALL:ALL) list")) 48 | .user(User(USERNAME).password(PASSWORD)) 49 | .user(OTHER_USERNAME) 50 | .hostname(HOSTNAME) 51 | .build(); 52 | 53 | let output = Command::new("sudo") 54 | .args(["-S", "-l", "-U", OTHER_USERNAME]) 55 | .stdin(PASSWORD) 56 | .as_user(USERNAME) 57 | .output(&env); 58 | 59 | assert!(!output.status().success()); 60 | assert_contains!( 61 | output.stderr(), 62 | format!( 63 | "Sorry, user {USERNAME} is not allowed to execute 'list' as {OTHER_USERNAME} on {HOSTNAME}." 64 | ) 65 | ); 66 | } 67 | 68 | #[test] 69 | fn invoking_user_has_list_perms_but_other_user_does_not_have_sudo_perms() { 70 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) NOPASSWD: list")) 71 | .user(User(USERNAME).password(PASSWORD)) 72 | .user(OTHER_USERNAME) 73 | .hostname(HOSTNAME) 74 | .build(); 75 | 76 | let output = Command::new("sudo") 77 | .args(["-S", "-l", "-U", OTHER_USERNAME]) 78 | .stdin(PASSWORD) 79 | .as_user(USERNAME) 80 | .output(&env); 81 | 82 | assert_contains!( 83 | output.stdout(), 84 | format!("User {OTHER_USERNAME} is not allowed to run sudo on {HOSTNAME}.") 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/flag_version.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env}; 2 | 3 | use crate::{Result, PANIC_EXIT_CODE}; 4 | 5 | #[test] 6 | fn does_not_panic_on_io_errors() -> Result<()> { 7 | let env = Env("").build(); 8 | 9 | let output = Command::new("bash") 10 | .args([ 11 | "-c", 12 | "sudo --version 2>&1 | true; echo \"${PIPESTATUS[0]}\"", 13 | ]) 14 | .output(&env); 15 | 16 | let exit_code = output.stdout().parse()?; 17 | assert_ne!(PANIC_EXIT_CODE, exit_code); 18 | assert_eq!(0, exit_code); 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/lecture_file.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | OG_SUDO_STANDARD_LECTURE, PASSWORD, SUDOERS_NEW_LECTURE, SUDOERS_NEW_LECTURE_USER, 3 | SUDOERS_ONCE_LECTURE, SUDOERS_ROOT_ALL, USERNAME, 4 | }; 5 | use sudo_test::{Command, Env, User}; 6 | 7 | #[ignore = "gh399"] 8 | #[test] 9 | fn default_lecture_message() { 10 | let env = Env([SUDOERS_ROOT_ALL, SUDOERS_ONCE_LECTURE]) 11 | .user(User(USERNAME).password(PASSWORD)) 12 | .build(); 13 | 14 | let output = Command::new("sudo") 15 | .args(["-S", "true"]) 16 | .as_user(USERNAME) 17 | .stdin(PASSWORD) 18 | .output(&env); 19 | 20 | assert_contains!(output.stderr(), OG_SUDO_STANDARD_LECTURE); 21 | } 22 | 23 | #[ignore = "gh400"] 24 | #[test] 25 | fn new_lecture_message() { 26 | let new_lecture = "I <3 sudo"; 27 | let env = Env([SUDOERS_ROOT_ALL, SUDOERS_ONCE_LECTURE, SUDOERS_NEW_LECTURE]) 28 | .file("/etc/sudo_lecture", new_lecture) 29 | .user(User(USERNAME).password(PASSWORD)) 30 | .build(); 31 | 32 | let output = Command::new("sudo") 33 | .as_user(USERNAME) 34 | .stdin(PASSWORD) 35 | .args(["-S", "true"]) 36 | .output(&env); 37 | assert!(!output.status().success()); 38 | assert_contains!(output.stderr(), "I <3 sudo"); 39 | } 40 | 41 | #[test] 42 | #[ignore = "gh400"] 43 | fn new_lecture_for_specific_user() { 44 | let new_lecture = "I <3 sudo"; 45 | let env = Env([ 46 | SUDOERS_ROOT_ALL, 47 | SUDOERS_ONCE_LECTURE, 48 | SUDOERS_NEW_LECTURE_USER, 49 | ]) 50 | .file("/etc/sudo_lecture", new_lecture) 51 | .user(User(USERNAME).password(PASSWORD)) 52 | .build(); 53 | 54 | let output = Command::new("sudo") 55 | .as_user(USERNAME) 56 | .stdin(PASSWORD) 57 | .args(["-S", "true"]) 58 | .output(&env); 59 | assert!(!output.status().success()); 60 | assert_contains!(output.stderr(), "I <3 sudo"); 61 | } 62 | 63 | #[ignore = "gh400"] 64 | #[test] 65 | fn default_lecture_for_unspecified_user() { 66 | let new_lecture = "I <3 sudo"; 67 | let env = Env([ 68 | SUDOERS_ROOT_ALL, 69 | SUDOERS_ONCE_LECTURE, 70 | SUDOERS_NEW_LECTURE_USER, 71 | ]) 72 | .file("/etc/sudo_lecture", new_lecture) 73 | .user(User(USERNAME).password(PASSWORD)) 74 | .user(User("other_user").password("other_password")) 75 | .build(); 76 | 77 | let output = Command::new("sudo") 78 | .as_user("other_user") 79 | .stdin("other_password") 80 | .args(["-S", "true"]) 81 | .output(&env); 82 | assert!(!output.status().success()); 83 | assert_contains!(output.stderr(), OG_SUDO_STANDARD_LECTURE); 84 | } 85 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/misc/read-parents-open-file-descriptor.bash: -------------------------------------------------------------------------------- 1 | echo topsecret >/tmp/secret.txt 2 | exec 42<>/tmp/secret.txt 3 | 4 | sudo bash -c 'cat <&42' 5 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/pam.rs: -------------------------------------------------------------------------------- 1 | //! PAM integration tests 2 | 3 | use sudo_test::{Command, Env, User}; 4 | 5 | use crate::{PASSWORD, USERNAME}; 6 | 7 | mod env; 8 | 9 | #[test] 10 | fn given_pam_permit_then_no_password_auth_required() { 11 | let env = Env("ALL ALL=(ALL:ALL) ALL") 12 | .user(USERNAME) 13 | .file("/etc/pam.d/sudo", "auth sufficient pam_permit.so") 14 | .build(); 15 | 16 | Command::new("sudo") 17 | .arg("true") 18 | .as_user(USERNAME) 19 | .output(&env) 20 | .assert_success(); 21 | } 22 | 23 | #[test] 24 | fn given_pam_deny_then_password_auth_always_fails() { 25 | let env = Env("ALL ALL=(ALL:ALL) ALL") 26 | .user(User(USERNAME).password(PASSWORD)) 27 | .file("/etc/pam.d/sudo", "auth requisite pam_deny.so") 28 | .build(); 29 | 30 | let output = Command::new("sudo") 31 | .args(["-S", "true"]) 32 | .as_user(USERNAME) 33 | .stdin(PASSWORD) 34 | .output(&env); 35 | 36 | output.assert_exit_code(1); 37 | 38 | let diagnostic = if sudo_test::is_original_sudo() { 39 | "3 incorrect password attempts" 40 | } else { 41 | "3 incorrect authentication attempts" 42 | }; 43 | assert_contains!(output.stderr(), diagnostic); 44 | } 45 | 46 | #[test] 47 | fn being_root_has_precedence_over_pam() { 48 | let env = Env("ALL ALL=(ALL:ALL) ALL") 49 | .file("/etc/pam.d/sudo", "auth requisite pam_deny.so") 50 | .build(); 51 | 52 | Command::new("sudo") 53 | .args(["true"]) 54 | .output(&env) 55 | .assert_success(); 56 | } 57 | 58 | #[test] 59 | fn nopasswd_in_sudoers_has_precedence_over_pam() { 60 | let env = Env("ALL ALL=(ALL:ALL) NOPASSWD: ALL") 61 | .file("/etc/pam.d/sudo", "auth requisite pam_deny.so") 62 | .user(USERNAME) 63 | .build(); 64 | 65 | Command::new("sudo") 66 | .arg("true") 67 | .as_user(USERNAME) 68 | .output(&env) 69 | .assert_success(); 70 | } 71 | 72 | #[test] 73 | fn sudo_uses_correct_service_file() { 74 | let env = Env("ALL ALL=(ALL:ALL) ALL") 75 | .file("/etc/pam.d/sudo", "auth sufficient pam_permit.so") 76 | .file("/etc/pam.d/sudo-i", "auth requisite pam_deny.so") 77 | .user(USERNAME) 78 | .build(); 79 | 80 | Command::new("sudo") 81 | .arg("true") 82 | .as_user(USERNAME) 83 | .output(&env) 84 | .assert_success(); 85 | } 86 | 87 | #[test] 88 | #[cfg_attr( 89 | target_os = "freebsd", 90 | ignore = "FreeBSD doesn't use sudo-i PAM context" 91 | )] 92 | fn sudo_dash_i_uses_correct_service_file() { 93 | let env = Env("ALL ALL=(ALL:ALL) ALL") 94 | .file("/etc/pam.d/sudo-i", "auth sufficient pam_permit.so") 95 | .file("/etc/pam.d/sudo", "auth requisite pam_deny.so") 96 | .user(USERNAME) 97 | .build(); 98 | 99 | Command::new("sudo") 100 | .args(["-i", "true"]) 101 | .as_user(USERNAME) 102 | .output(&env) 103 | .assert_success(); 104 | } 105 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/pass_auth.rs: -------------------------------------------------------------------------------- 1 | //! Scenarios where password authentication is needed 2 | 3 | // NOTE all these tests assume that the invoking user passes the sudoers file 'User_List' criteria 4 | 5 | mod stdin; 6 | mod tty; 7 | 8 | #[cfg(not(target_os = "freebsd"))] 9 | const MAX_PASSWORD_SIZE: usize = 511; // MAX_PAM_RESPONSE_SIZE - 1 null byte 10 | #[cfg(target_os = "freebsd")] 11 | const MAX_PASSWORD_SIZE: usize = 128; 12 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/passwd.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, BIN_LS, BIN_TRUE}; 2 | 3 | use crate::{SUDOERS_NO_LECTURE, USERNAME}; 4 | 5 | macro_rules! assert_snapshot { 6 | ($($tt:tt)*) => { 7 | insta::with_settings!({ 8 | filters => if cfg!(target_os = "linux") { 9 | vec![(r"[[:xdigit:]]{12}", "[host]")] 10 | } else { 11 | vec![ 12 | (r"[[:xdigit:]]{12}", "[host]"), 13 | ("Password:", "[sudo] password for ferris: "), 14 | ] 15 | }, 16 | prepend_module_to_snapshot => false, 17 | snapshot_path => "../snapshots/passwd", 18 | }, { 19 | insta::assert_snapshot!($($tt)*) 20 | }); 21 | }; 22 | } 23 | 24 | #[test] 25 | fn explicit_passwd_overrides_nopasswd() { 26 | let env = Env([ 27 | format!("ALL ALL=(ALL:ALL) NOPASSWD: {BIN_TRUE}, PASSWD: {BIN_LS}"), 28 | SUDOERS_NO_LECTURE.to_owned(), 29 | ]) 30 | .user(USERNAME) 31 | .build(); 32 | 33 | let output = Command::new("sudo") 34 | .arg("true") 35 | .as_user(USERNAME) 36 | .output(&env); 37 | 38 | output.assert_success(); 39 | 40 | let second_output = Command::new("sudo") 41 | .args(["-S", "ls"]) 42 | .as_user(USERNAME) 43 | .output(&env); 44 | 45 | second_output.assert_exit_code(1); 46 | 47 | let stderr = second_output.stderr(); 48 | if sudo_test::is_original_sudo() { 49 | assert_snapshot!(stderr); 50 | } else { 51 | assert_contains!(stderr, "Maximum 3 incorrect authentication attempts"); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/password_retry/time-password-retry.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | date +%s%3N 4 | ( 5 | echo wrong-password 6 | echo strong-password 7 | ) | sudo -S date +%s%3N 8 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/perms.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | use sudo_test::{Command, Env, TextFile}; 3 | 4 | use crate::{SUDOERS_USER_ALL_NOPASSWD, USERNAME}; 5 | 6 | #[test] 7 | fn user_can_read_file_owned_by_root() { 8 | let expected = "hello"; 9 | let path = "/root/file"; 10 | let env = Env(SUDOERS_USER_ALL_NOPASSWD) 11 | .user(USERNAME) 12 | .file(path, expected) 13 | .build(); 14 | 15 | let actual = Command::new("sudo") 16 | .args(["cat", path]) 17 | .as_user(USERNAME) 18 | .output(&env) 19 | .stdout(); 20 | assert_eq!(expected, actual); 21 | } 22 | 23 | #[test] 24 | fn user_can_write_file_owned_by_root() { 25 | let path = "/root/file"; 26 | let env = Env(SUDOERS_USER_ALL_NOPASSWD) 27 | .user(USERNAME) 28 | .file(path, "") 29 | .build(); 30 | 31 | Command::new("sudo") 32 | .args(["rm", path]) 33 | .as_user(USERNAME) 34 | .output(&env) 35 | .assert_success(); 36 | } 37 | 38 | #[test] 39 | fn user_can_execute_file_owned_by_root() { 40 | let path = "/root/file"; 41 | let env = Env(SUDOERS_USER_ALL_NOPASSWD) 42 | .user(USERNAME) 43 | .file( 44 | path, 45 | TextFile( 46 | r#"#!/bin/sh 47 | exit 0"#, 48 | ) 49 | .chmod("100"), 50 | ) 51 | .build(); 52 | 53 | Command::new("sudo") 54 | .arg(path) 55 | .as_user(USERNAME) 56 | .output(&env) 57 | .assert_success(); 58 | } 59 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/sudoers/host_list.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env}; 2 | 3 | use crate::LONGEST_HOSTNAME; 4 | 5 | macro_rules! assert_snapshot { 6 | ($($tt:tt)*) => { 7 | insta::with_settings!({ 8 | prepend_module_to_snapshot => false, 9 | snapshot_path => "../../snapshots/sudoers/host_list", 10 | }, { 11 | insta::assert_snapshot!($($tt)*) 12 | }); 13 | }; 14 | } 15 | 16 | #[test] 17 | fn given_specific_hostname_then_sudo_from_said_hostname_is_allowed() { 18 | let hostname = "container"; 19 | let env = Env(format!("ALL {hostname} = (ALL:ALL) ALL")) 20 | .hostname(hostname) 21 | .build(); 22 | 23 | Command::new("sudo") 24 | .arg("true") 25 | .output(&env) 26 | .assert_success(); 27 | } 28 | 29 | #[test] 30 | fn given_specific_hostname_then_sudo_from_different_hostname_is_rejected() { 31 | let env = Env("ALL remotehost = (ALL:ALL) ALL") 32 | .hostname("container") 33 | .build(); 34 | 35 | let output = Command::new("sudo").arg("true").output(&env); 36 | 37 | output.assert_exit_code(1); 38 | 39 | let stderr = output.stderr(); 40 | if sudo_test::is_original_sudo() { 41 | assert_snapshot!(stderr); 42 | } else { 43 | assert_contains!(stderr, "I'm sorry root. I'm afraid I can't do that"); 44 | } 45 | } 46 | 47 | #[test] 48 | fn different() { 49 | let env = Env("ALL remotehost, container = (ALL:ALL) ALL") 50 | .hostname("container") 51 | .build(); 52 | 53 | Command::new("sudo") 54 | .arg("true") 55 | .output(&env) 56 | .assert_success(); 57 | } 58 | 59 | #[test] 60 | fn repeated() { 61 | let env = Env("ALL container, container = (ALL:ALL) ALL") 62 | .hostname("container") 63 | .build(); 64 | 65 | Command::new("sudo") 66 | .arg("true") 67 | .output(&env) 68 | .assert_success(); 69 | } 70 | 71 | #[test] 72 | fn negation_rejects() { 73 | let env = Env("ALL remotehost, !container = (ALL:ALL) ALL") 74 | .hostname("container") 75 | .build(); 76 | 77 | let output = Command::new("sudo").arg("true").output(&env); 78 | 79 | output.assert_exit_code(1); 80 | 81 | let stderr = output.stderr(); 82 | if sudo_test::is_original_sudo() { 83 | assert_snapshot!(stderr); 84 | } else { 85 | assert_contains!(stderr, "I'm sorry root. I'm afraid I can't do that"); 86 | } 87 | } 88 | 89 | #[test] 90 | fn double_negative_is_positive() { 91 | let env = Env("ALL !!container = (ALL:ALL) ALL") 92 | .hostname("container") 93 | .build(); 94 | 95 | Command::new("sudo") 96 | .arg("true") 97 | .output(&env) 98 | .assert_success(); 99 | } 100 | 101 | #[test] 102 | fn longest_hostname() { 103 | let env = Env(format!("ALL {LONGEST_HOSTNAME} = (ALL:ALL) ALL")) 104 | .hostname(LONGEST_HOSTNAME) 105 | .build(); 106 | 107 | Command::new("sudo") 108 | .arg("true") 109 | .output(&env) 110 | .assert_success(); 111 | } 112 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/sudoers/timestamp_timeout.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, User}; 2 | 3 | use crate::{PASSWORD, USERNAME}; 4 | 5 | #[test] 6 | fn credential_caching_works_with_custom_timeout() { 7 | let env = Env(format!( 8 | "{USERNAME} ALL=(ALL:ALL) ALL 9 | Defaults timestamp_timeout=0.1" 10 | )) 11 | .user(User(USERNAME).password(PASSWORD)) 12 | .build(); 13 | 14 | // input valid credentials 15 | // try to sudo without a password 16 | Command::new("sh") 17 | .arg("-c") 18 | .arg(format!("echo {PASSWORD} | sudo -S true; sudo true && true")) 19 | .as_user(USERNAME) 20 | .output(&env) 21 | .assert_success(); 22 | } 23 | 24 | #[test] 25 | fn nonzero() { 26 | let env = Env(format!( 27 | "{USERNAME} ALL=(ALL:ALL) ALL 28 | Defaults timestamp_timeout=0.1" 29 | )) 30 | .user(User(USERNAME).password(PASSWORD)) 31 | .build(); 32 | 33 | // input valid credentials 34 | // wait until they expire / timeout 35 | // try to sudo without a password 36 | let output = Command::new("sh") 37 | .arg("-c") 38 | .arg(format!( 39 | "echo {PASSWORD} | sudo -S true; sleep 10; sudo true && true" 40 | )) 41 | .as_user(USERNAME) 42 | .output(&env); 43 | 44 | output.assert_exit_code(1); 45 | 46 | let diagnostic = if sudo_test::is_original_sudo() { 47 | "a password is required" 48 | } else { 49 | "incorrect authentication attempt" 50 | }; 51 | assert_contains!(output.stderr(), diagnostic); 52 | } 53 | 54 | #[test] 55 | fn zero_always_prompts_for_password() { 56 | let env = Env(format!( 57 | "{USERNAME} ALL=(ALL:ALL) ALL 58 | Defaults timestamp_timeout=0" 59 | )) 60 | .user(User(USERNAME).password(PASSWORD)) 61 | .build(); 62 | 63 | // input valid credentials 64 | // try to sudo without a password 65 | let output = Command::new("sh") 66 | .arg("-c") 67 | .arg(format!("echo {PASSWORD} | sudo -S true; sudo true && true")) 68 | .as_user(USERNAME) 69 | .output(&env); 70 | 71 | output.assert_exit_code(1); 72 | 73 | let diagnostic = if sudo_test::is_original_sudo() { 74 | "a password is required" 75 | } else { 76 | "incorrect authentication attempt" 77 | }; 78 | assert_contains!(output.stderr(), diagnostic); 79 | } 80 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/syslog.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, BIN_TRUE}; 2 | 3 | use crate::{helpers::Rsyslogd, SUDOERS_ALL_ALL_NOPASSWD, SUDOERS_USER_ALL_ALL, USERNAME}; 4 | 5 | #[test] 6 | fn sudo_logs_every_executed_command() { 7 | let env = Env(SUDOERS_ALL_ALL_NOPASSWD).build(); 8 | let rsyslog = Rsyslogd::start(&env); 9 | 10 | let auth_log = rsyslog.auth_log(); 11 | assert_eq!("", auth_log); 12 | 13 | Command::new("sudo") 14 | .arg("true") 15 | .output(&env) 16 | .assert_success(); 17 | 18 | let auth_log = rsyslog.auth_log(); 19 | assert_contains!(auth_log, format!("COMMAND={BIN_TRUE}")); 20 | } 21 | 22 | #[test] 23 | #[cfg_attr( 24 | target_os = "freebsd", 25 | ignore = "Logging not really functional on FreeBSD even with og-sudo" 26 | )] 27 | fn sudo_logs_every_failed_authentication_attempt() { 28 | let env = Env(SUDOERS_USER_ALL_ALL).user(USERNAME).build(); 29 | let rsyslog = Rsyslogd::start(&env); 30 | 31 | let auth_log = rsyslog.auth_log(); 32 | assert_eq!("", auth_log); 33 | 34 | let output = Command::new("sudo") 35 | .args(["-S", "true"]) 36 | .as_user(USERNAME) 37 | .output(&env); 38 | 39 | assert!(!output.status().success()); 40 | 41 | let auth_log = rsyslog.auth_log(); 42 | let diagnostic = if sudo_test::is_original_sudo() { 43 | "auth could not identify password" 44 | } else { 45 | "authentication failure" 46 | }; 47 | assert_contains!(auth_log, diagnostic); 48 | } 49 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, User}; 2 | 3 | use crate::{PASSWORD, USERNAME}; 4 | 5 | #[test] 6 | fn is_limited_to_a_single_user() { 7 | let second_user = "ghost"; 8 | let env = Env("ALL ALL=(ALL:ALL) ALL") 9 | .user(User(USERNAME).password(PASSWORD)) 10 | .user(User(second_user).password(PASSWORD)) 11 | .build(); 12 | 13 | let child = Command::new("sh") 14 | .arg("-c") 15 | .arg(format!( 16 | "echo {PASSWORD} | sudo -S true; touch /tmp/barrier1; until [ -f /tmp/barrier2 ]; do sleep 1; done; sudo -S true && true" 17 | )) 18 | .as_user(USERNAME) 19 | .spawn(&env); 20 | 21 | Command::new("sh") 22 | .arg("-c") 23 | .arg("until [ -f /tmp/barrier1 ]; do sleep 1; done; sudo -K && touch /tmp/barrier2") 24 | .as_user(second_user) 25 | .output(&env) 26 | .assert_success(); 27 | 28 | child.wait().assert_success(); 29 | } 30 | 31 | #[test] 32 | fn has_a_user_global_effect() { 33 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) ALL")) 34 | .user(User(USERNAME).password(PASSWORD)) 35 | .build(); 36 | 37 | let child = Command::new("sh") 38 | .arg("-c") 39 | .arg(format!( 40 | "echo {PASSWORD} | sudo -S true; touch /tmp/barrier1; until [ -f /tmp/barrier2 ]; do sleep 1; done; echo | sudo -S true && true" 41 | )) 42 | .as_user(USERNAME) 43 | .spawn(&env); 44 | 45 | Command::new("sh") 46 | .arg("-c") 47 | .arg("until [ -f /tmp/barrier1 ]; do sleep 1; done; sudo -K && touch /tmp/barrier2") 48 | .as_user(USERNAME) 49 | .output(&env) 50 | .assert_success(); 51 | 52 | let output = child.wait(); 53 | 54 | output.assert_exit_code(1); 55 | 56 | let diagnostic = if sudo_test::is_original_sudo() { 57 | "1 incorrect password attempt" 58 | } else { 59 | "incorrect authentication attempt" 60 | }; 61 | assert_contains!(output.stderr(), diagnostic); 62 | } 63 | 64 | #[test] 65 | fn also_works_locally() { 66 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) ALL")) 67 | .user(User(USERNAME).password(PASSWORD)) 68 | .build(); 69 | 70 | // input valid credentials 71 | // invalidate them 72 | // try to sudo without a password 73 | let output = Command::new("sh") 74 | .arg("-c") 75 | .arg(format!( 76 | "echo {PASSWORD} | sudo -S true; sudo -K; sudo true && true" 77 | )) 78 | .as_user(USERNAME) 79 | .output(&env); 80 | 81 | output.assert_exit_code(1); 82 | 83 | let diagnostic = if sudo_test::is_original_sudo() { 84 | "a password is required" 85 | } else { 86 | "Authentication failed" 87 | }; 88 | assert_contains!(output.stderr(), diagnostic); 89 | } 90 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/sudo/timestamp/validate.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, User}; 2 | 3 | use crate::{PASSWORD, USERNAME}; 4 | 5 | #[test] 6 | fn revalidation() { 7 | let env = Env(format!( 8 | "{USERNAME} ALL=(ALL:ALL) ALL 9 | Defaults timestamp_timeout=0.1" 10 | )) 11 | .user(User(USERNAME).password(PASSWORD)) 12 | .build(); 13 | 14 | // input valid credentials 15 | // revalidate credentials a few times 16 | // sudo without a password, using re-validated credentials 17 | Command::new("sh") 18 | .arg("-c") 19 | .arg(format!( 20 | "set -e; echo {PASSWORD} | sudo -S true; for i in $(seq 1 5); do sleep 3; sudo -v; done; sudo true && true" 21 | )) 22 | .as_user(USERNAME) 23 | .output(&env) 24 | .assert_success(); 25 | } 26 | 27 | #[test] 28 | fn prompts_for_password() { 29 | let env = Env(format!("{USERNAME} ALL=(ALL:ALL) ALL")) 30 | .user(User(USERNAME).password(PASSWORD)) 31 | .build(); 32 | 33 | let output = Command::new("sudo") 34 | .arg("-v") 35 | .as_user(USERNAME) 36 | .output(&env); 37 | 38 | output.assert_exit_code(1); 39 | 40 | let diagnostic = if sudo_test::is_original_sudo() { 41 | "a password is required" 42 | } else { 43 | "incorrect authentication attempt" 44 | }; 45 | assert_contains!(output.stderr(), diagnostic); 46 | } 47 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/flag_help.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env}; 2 | 3 | use crate::USERNAME; 4 | 5 | #[test] 6 | fn prints_to_stdout() { 7 | let env = Env("").user(USERNAME).build(); 8 | 9 | let long = Command::new("visudo") 10 | .arg("--help") 11 | .as_user(USERNAME) 12 | .output(&env) 13 | .stdout(); 14 | 15 | let short = Command::new("visudo") 16 | .arg("-h") 17 | .as_user(USERNAME) 18 | .output(&env) 19 | .stdout(); 20 | 21 | assert_eq!(short, long); 22 | assert_contains!(short, "visudo - safely edit the sudoers file"); 23 | } 24 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/flag_no_includes.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, TextFile, ETC_DIR}; 2 | 3 | use crate::visudo::{CHMOD_EXEC, DEFAULT_EDITOR, LOGS_PATH}; 4 | 5 | #[test] 6 | fn does_not_edit_at_include_files_that_dont_contain_syntax_errors() { 7 | let env = Env("# 1 8 | @include sudoers2") 9 | .file(format!("{ETC_DIR}/sudoers2"), "# 2") 10 | .file( 11 | DEFAULT_EDITOR, 12 | TextFile(format!( 13 | "#!/bin/sh 14 | cat $2 >> {LOGS_PATH}" 15 | )) 16 | .chmod(CHMOD_EXEC), 17 | ) 18 | .build(); 19 | 20 | Command::new("visudo") 21 | .arg("--no-includes") 22 | .output(&env) 23 | .assert_success(); 24 | let logs = Command::new("cat").arg(LOGS_PATH).output(&env).stdout(); 25 | 26 | let comments = logs 27 | .lines() 28 | .filter(|line| line.starts_with('#')) 29 | .collect::>(); 30 | 31 | assert_eq!(["# 1"], &*comments); 32 | } 33 | 34 | #[test] 35 | fn does_edit_at_include_files_that_contain_syntax_errors() { 36 | let env = Env("# 1 37 | @include sudoers2") 38 | .file( 39 | format!("{ETC_DIR}/sudoers2"), 40 | "# 2 41 | this is fine", 42 | ) 43 | .file( 44 | DEFAULT_EDITOR, 45 | TextFile(format!( 46 | "#!/bin/sh 47 | cat $2 >> {LOGS_PATH}" 48 | )) 49 | .chmod(CHMOD_EXEC), 50 | ) 51 | .build(); 52 | 53 | Command::new("visudo") 54 | .arg("--no-includes") 55 | .output(&env) 56 | .assert_success(); 57 | let logs = Command::new("cat").arg(LOGS_PATH).output(&env).stdout(); 58 | 59 | let comments = logs 60 | .lines() 61 | .filter(|line| line.starts_with('#')) 62 | .collect::>(); 63 | 64 | assert_eq!(["# 1"], &*comments); 65 | } 66 | 67 | #[test] 68 | fn does_not_edit_deep_at_include_files_that_contain_syntax_errors() { 69 | let env = Env("# 1 70 | @include sudoers2") 71 | .file( 72 | format!("{ETC_DIR}/sudoers2"), 73 | "# 2 74 | @include sudoers3", 75 | ) 76 | .file( 77 | format!("{ETC_DIR}/sudoers3"), 78 | "# 3 79 | this is fine", 80 | ) 81 | .file( 82 | DEFAULT_EDITOR, 83 | TextFile(format!( 84 | "#!/bin/sh 85 | cat $2 >> {LOGS_PATH}" 86 | )) 87 | .chmod(CHMOD_EXEC), 88 | ) 89 | .build(); 90 | 91 | Command::new("visudo") 92 | .arg("--no-includes") 93 | .output(&env) 94 | .assert_success(); 95 | let logs = Command::new("cat").arg(LOGS_PATH).output(&env).stdout(); 96 | 97 | let comments = logs 98 | .lines() 99 | .filter(|line| line.starts_with('#')) 100 | .collect::>(); 101 | 102 | assert_eq!(["# 1"], &*comments); 103 | } 104 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/flag_owner.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, TextFile, ROOT_GROUP}; 2 | 3 | use crate::{ 4 | visudo::{CHMOD_EXEC, DEFAULT_EDITOR, EDITOR_DUMMY, ETC_SUDOERS, TMP_SUDOERS}, 5 | USERNAME, 6 | }; 7 | 8 | #[test] 9 | fn when_present_changes_ownership_of_existing_file() { 10 | let file_path = TMP_SUDOERS; 11 | let env = Env("") 12 | .file(file_path, TextFile("").chown("root:users").chmod("777")) 13 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 14 | .build(); 15 | 16 | Command::new("visudo") 17 | .args(["--owner", "--file", file_path]) 18 | .output(&env) 19 | .assert_success(); 20 | 21 | let ls_output = Command::new("ls") 22 | .args(["-l", file_path]) 23 | .output(&env) 24 | .stdout(); 25 | 26 | assert_contains!(ls_output, format!(" root {ROOT_GROUP} ")); 27 | } 28 | 29 | #[test] 30 | fn when_absent_ownership_is_preserved() { 31 | let file_path = TMP_SUDOERS; 32 | let env = Env("") 33 | .file(file_path, TextFile("").chown("root:users").chmod("777")) 34 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 35 | .build(); 36 | 37 | Command::new("visudo") 38 | .args(["--file", file_path]) 39 | .output(&env) 40 | .assert_success(); 41 | 42 | let ls_output = Command::new("ls") 43 | .args(["-l", file_path]) 44 | .output(&env) 45 | .stdout(); 46 | 47 | assert_contains!(ls_output, " root users "); 48 | } 49 | 50 | #[test] 51 | fn etc_sudoers_ownership_is_always_changed() { 52 | let file_path = ETC_SUDOERS; 53 | let env = Env(TextFile("").chown(format!("{USERNAME}:users")).chmod("777")) 54 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 55 | .user(USERNAME) 56 | .build(); 57 | 58 | Command::new("visudo").output(&env).assert_success(); 59 | 60 | let ls_output = Command::new("ls") 61 | .args(["-l", file_path]) 62 | .output(&env) 63 | .stdout(); 64 | 65 | assert_contains!(ls_output, format!(" root {ROOT_GROUP} ")); 66 | } 67 | 68 | #[test] 69 | fn flag_check() { 70 | let file_path = TMP_SUDOERS; 71 | let env = Env("") 72 | .file( 73 | file_path, 74 | TextFile("").chown(format!("{USERNAME}:users")).chmod("777"), 75 | ) 76 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 77 | .user(USERNAME) 78 | .build(); 79 | 80 | let output = Command::new("visudo") 81 | .args(["--check", "--owner", "--file", file_path]) 82 | .output(&env); 83 | 84 | output.assert_exit_code(1); 85 | assert_contains!( 86 | output.stderr(), 87 | format!("{file_path}: wrong owner (uid, gid) should be (0, 0)") 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/flag_perms.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, TextFile}; 2 | 3 | use crate::{ 4 | visudo::{CHMOD_EXEC, DEFAULT_EDITOR, EDITOR_DUMMY, ETC_SUDOERS, TMP_SUDOERS}, 5 | USERNAME, 6 | }; 7 | 8 | #[test] 9 | fn when_present_changes_perms_of_existing_file() { 10 | let file_path = TMP_SUDOERS; 11 | let env = Env("") 12 | .file(file_path, TextFile("").chmod("777")) 13 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 14 | .build(); 15 | 16 | Command::new("visudo") 17 | .args(["--perms", "--file", file_path]) 18 | .output(&env) 19 | .assert_success(); 20 | 21 | let ls_output = Command::new("ls") 22 | .args(["-l", file_path]) 23 | .output(&env) 24 | .stdout(); 25 | 26 | assert!(ls_output.starts_with("-r--r----- ")); 27 | } 28 | 29 | #[test] 30 | fn when_absent_perms_are_preserved() { 31 | let file_path = TMP_SUDOERS; 32 | let env = Env("") 33 | .file(file_path, TextFile("").chmod("777")) 34 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 35 | .build(); 36 | 37 | Command::new("visudo") 38 | .args(["--file", file_path]) 39 | .output(&env) 40 | .assert_success(); 41 | 42 | let ls_output = Command::new("ls") 43 | .args(["-l", file_path]) 44 | .output(&env) 45 | .stdout(); 46 | 47 | assert!(ls_output.starts_with("-rwxrwxrwx ")); 48 | } 49 | 50 | #[test] 51 | fn etc_sudoers_perms_are_always_changed() { 52 | let file_path = ETC_SUDOERS; 53 | let env = Env(TextFile("").chmod("777")) 54 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 55 | .build(); 56 | 57 | Command::new("visudo").output(&env).assert_success(); 58 | 59 | let ls_output = Command::new("ls") 60 | .args(["-l", file_path]) 61 | .output(&env) 62 | .stdout(); 63 | 64 | assert!(ls_output.starts_with("-r--r----- ")); 65 | } 66 | 67 | #[test] 68 | fn flag_check() { 69 | let file_path = TMP_SUDOERS; 70 | let env = Env("") 71 | .file( 72 | file_path, 73 | TextFile("").chown(format!("{USERNAME}:users")).chmod("777"), 74 | ) 75 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 76 | .user(USERNAME) 77 | .build(); 78 | 79 | let output = Command::new("visudo") 80 | .args(["--check", "--perms", "--file", file_path]) 81 | .output(&env); 82 | 83 | output.assert_exit_code(1); 84 | assert_contains!( 85 | output.stderr(), 86 | format!("{file_path}: bad permissions, should be mode 0440") 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/flag_quiet.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, TextFile}; 2 | 3 | use super::{CHMOD_EXEC, DEFAULT_EDITOR, EDITOR_DUMMY}; 4 | 5 | #[test] 6 | #[ignore = "gh657"] 7 | fn supresses_syntax_error_messages() { 8 | let env = Env("this is fine") 9 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 10 | .build(); 11 | 12 | let output = Command::new("visudo").arg("-q").output(&env); 13 | 14 | output.assert_success(); 15 | assert_not_contains!(output.stderr(), "syntax error"); 16 | } 17 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/flag_strict.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env, TextFile}; 2 | 3 | use crate::visudo::{CHMOD_EXEC, DEFAULT_EDITOR, EDITOR_DUMMY}; 4 | 5 | #[test] 6 | #[ignore = "gh657"] 7 | fn undefined_alias() { 8 | let env = Env(["# User_Alias ADMINS = root", "ADMINS ALL=(ALL:ALL) ALL"]) 9 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 10 | .build(); 11 | 12 | let output = Command::new("visudo").arg("--strict").output(&env); 13 | 14 | let diagnostic = r#"User_Alias "ADMINS" referenced but not defined"#; 15 | let prompt = "What now?"; 16 | 17 | output.assert_success(); 18 | assert_contains!(output.stderr(), diagnostic); 19 | // we only get this prompt in `--strict` mode 20 | assert_contains!(output.stdout(), prompt); 21 | 22 | let output = Command::new("visudo").output(&env); 23 | 24 | output.assert_success(); 25 | assert_contains!(output.stderr(), diagnostic); 26 | assert_not_contains!(output.stdout(), prompt); 27 | } 28 | 29 | #[test] 30 | fn alias_cycle() { 31 | let env = Env(["User_Alias FOO = FOO", "FOO ALL=(ALL:ALL) ALL"]) 32 | .file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC)) 33 | .build(); 34 | 35 | let output = Command::new("visudo").arg("--strict").output(&env); 36 | 37 | let diagnostic = if sudo_test::is_original_sudo() { 38 | r#"cycle in User_Alias "FOO""# 39 | } else { 40 | "syntax error: recursive alias: 'FOO'" 41 | }; 42 | let prompt = "What now?"; 43 | 44 | output.assert_success(); 45 | assert_contains!(output.stderr(), diagnostic); 46 | // we only get this prompt in `--strict` mode 47 | assert_contains!(output.stdout(), prompt); 48 | 49 | let output = Command::new("visudo").output(&env); 50 | 51 | output.assert_success(); 52 | assert_contains!(output.stderr(), diagnostic); 53 | if sudo_test::is_original_sudo() { 54 | assert_not_contains!(output.stdout(), prompt); 55 | } else { 56 | // visudo-rs is always strict 57 | assert_contains!(output.stdout(), prompt); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/flag_version.rs: -------------------------------------------------------------------------------- 1 | use sudo_test::{Command, Env}; 2 | 3 | use crate::USERNAME; 4 | 5 | #[test] 6 | fn prints_to_stdout() { 7 | let env = Env("").user(USERNAME).build(); 8 | 9 | let long = Command::new("visudo") 10 | .arg("--version") 11 | .as_user(USERNAME) 12 | .output(&env) 13 | .stdout(); 14 | 15 | let short = Command::new("visudo") 16 | .arg("-V") 17 | .as_user(USERNAME) 18 | .output(&env) 19 | .stdout(); 20 | 21 | assert_eq!(short, long); 22 | 23 | assert_contains!(short, "visudo version"); 24 | 25 | if sudo_test::is_original_sudo() { 26 | assert_contains!(short, "visudo grammar version 50"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/kill-visudo.sh: -------------------------------------------------------------------------------- 1 | until [ -f /tmp/barrier ]; do 2 | sleep 0.1 3 | done 4 | 5 | visudopid=$(pidof visudo | sort -gr | cut -f 1 -d ' ') 6 | 7 | if [ -n "$visudopid" ]; then 8 | kill $1 "$visudopid" 9 | fi 10 | -------------------------------------------------------------------------------- /test-framework/sudo-compliance-tests/src/visudo/sudoers.rs: -------------------------------------------------------------------------------- 1 | mod editor; 2 | mod env_editor; 3 | -------------------------------------------------------------------------------- /test-framework/sudo-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sudo-test" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | doctest = false 8 | 9 | [dependencies] 10 | tempfile = "3.4.0" 11 | 12 | [features] 13 | apparmor = [] 14 | -------------------------------------------------------------------------------- /test-framework/sudo-test/src/constants.rs: -------------------------------------------------------------------------------- 1 | /// The parent of the directory where sudo will look for the sudoers file. 2 | #[cfg(not(target_os = "freebsd"))] 3 | pub const ETC_PARENT_DIR: &str = "/"; 4 | 5 | /// The parent of the directory where sudo will look for the sudoers file. 6 | #[cfg(target_os = "freebsd")] 7 | pub const ETC_PARENT_DIR: &str = "/usr/local/"; 8 | 9 | /// The directory where sudo will look for the sudoers file. 10 | #[cfg(not(target_os = "freebsd"))] 11 | pub const ETC_DIR: &str = "/etc"; 12 | 13 | /// The directory where sudo will look for the sudoers file. 14 | #[cfg(target_os = "freebsd")] 15 | pub const ETC_DIR: &str = "/usr/local/etc"; 16 | 17 | /// The path where sudo will look for the sudoers file. 18 | #[cfg(not(target_os = "freebsd"))] 19 | pub const ETC_SUDOERS: &str = "/etc/sudoers"; 20 | 21 | /// The path where sudo will look for the sudoers file. 22 | #[cfg(target_os = "freebsd")] 23 | pub const ETC_SUDOERS: &str = "/usr/local/etc/sudoers"; 24 | 25 | /// The name of the primary group of the root user. 26 | #[cfg(not(target_os = "freebsd"))] 27 | pub const ROOT_GROUP: &str = "root"; 28 | 29 | /// The name of the primary group of the root user. 30 | #[cfg(target_os = "freebsd")] 31 | pub const ROOT_GROUP: &str = "wheel"; 32 | 33 | /// The path to the `sudo` binary. 34 | #[cfg(not(target_os = "freebsd"))] 35 | pub const BIN_SUDO: &str = "/usr/bin/sudo"; 36 | 37 | /// The path to the `sudo` binary. 38 | #[cfg(target_os = "freebsd")] 39 | pub const BIN_SUDO: &str = "/usr/local/bin/sudo"; 40 | 41 | /// The path to the `ls` binary. 42 | #[cfg(not(target_os = "freebsd"))] 43 | pub const BIN_LS: &str = "/usr/bin/ls"; 44 | 45 | /// The path to the `ls` binary. 46 | #[cfg(target_os = "freebsd")] 47 | pub const BIN_LS: &str = "/bin/ls"; 48 | 49 | /// The path to the `pwd` binary. 50 | #[cfg(not(target_os = "freebsd"))] 51 | pub const BIN_PWD: &str = "/usr/bin/pwd"; 52 | 53 | /// The path to the `pwd` binary. 54 | #[cfg(target_os = "freebsd")] 55 | pub const BIN_PWD: &str = "/bin/pwd"; 56 | 57 | /// The path to the `true` binary. 58 | pub const BIN_TRUE: &str = "/usr/bin/true"; 59 | 60 | /// The path to the `false` binary. 61 | pub const BIN_FALSE: &str = "/usr/bin/false"; 62 | 63 | /// The path to the `bash` binary. 64 | #[cfg(not(target_os = "freebsd"))] 65 | pub const BIN_BASH: &str = "/usr/bin/bash"; 66 | 67 | /// The path to the `bash` binary. 68 | #[cfg(target_os = "freebsd")] 69 | pub const BIN_BASH: &str = "/usr/local/bin/bash"; 70 | -------------------------------------------------------------------------------- /test-framework/sudo-test/src/helpers.rs: -------------------------------------------------------------------------------- 1 | //! Test helpers 2 | 3 | /// A command which will print the owner of it's TTY in the format "username group" 4 | #[cfg(target_os = "linux")] 5 | pub const PRINT_PTY_OWNER: &str = "stat $(tty) --format '%U %G'"; 6 | 7 | /// A command which will print the owner of it's TTY in the format "username group" 8 | #[cfg(target_os = "freebsd")] 9 | pub const PRINT_PTY_OWNER: &str = "stat -f '%Su %Sg' $(tty)"; 10 | 11 | /// Check that the ls output matches the expectation while being insensitive to the exact output of 12 | /// the system ls version. 13 | #[track_caller] 14 | pub fn assert_ls_output(ls_output: &str, mode: &str, user: &str, group: &str) { 15 | let parts = ls_output 16 | .split(' ') 17 | .filter(|part| !part.is_empty()) // FreeBSD ls often uses multiple spaces as separator 18 | .collect::>(); 19 | 20 | assert_eq!(parts[0], mode); 21 | assert_eq!(parts[2], user); 22 | assert_eq!(parts[3], group); 23 | } 24 | 25 | /// parse the output of `ps aux` 26 | pub fn parse_ps_aux(ps_aux: &str) -> Vec { 27 | let mut entries = vec![]; 28 | for line in ps_aux.lines().skip(1 /* header */) { 29 | let columns = line.split_ascii_whitespace().collect::>(); 30 | 31 | let entry = PsAuxEntry { 32 | command: columns[10..].join(" "), 33 | pid: columns[1].parse().expect("invalid PID"), 34 | process_state: columns[7].to_owned(), 35 | tty: columns[6].to_owned(), 36 | }; 37 | 38 | entries.push(entry); 39 | } 40 | 41 | entries 42 | } 43 | 44 | /// an entry / row in `ps aux` output 45 | #[derive(Debug)] 46 | pub struct PsAuxEntry { 47 | /// command column 48 | pub command: String, 49 | /// pid column 50 | pub pid: u32, 51 | /// process state column 52 | pub process_state: String, 53 | /// tty column 54 | pub tty: String, 55 | } 56 | 57 | impl PsAuxEntry { 58 | /// whether the process has an associated PTY 59 | pub fn has_tty(&self) -> bool { 60 | if cfg!(target_os = "linux") { 61 | // On Linux the PTY is either ? when there is no pty, starts with pts/ for a pseudo 62 | // terminal or starts with tty in case of a virtual terminal. The last case shouldn't 63 | // happen inside of containers. 64 | if self.tty == "?" { 65 | false 66 | } else if self.tty.starts_with("pts/") { 67 | true 68 | } else { 69 | unimplemented!() 70 | } 71 | } else if cfg!(target_os = "freebsd") { 72 | // On FreeBSD the PTY is either ? or - when there is no pty, or is an integer 73 | // potentially prefixed by v. 74 | !(self.tty == "?" || self.tty == "-") 75 | } else { 76 | unimplemented!() 77 | } 78 | } 79 | 80 | /// whethe the process is a session leader 81 | pub fn is_session_leader(&self) -> bool { 82 | self.process_state.contains('s') 83 | } 84 | 85 | /// whethe the process is in the foreground process group 86 | pub fn is_in_the_foreground_process_group(&self) -> bool { 87 | self.process_state.contains('+') 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test-framework/sudo-test/src/ours.freebsd.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dougrabson/freebsd14.1-small:latest 2 | RUN IGNORE_OSVERSION=yes pkg install -y sshpass rsyslog bash vim pidof dash 3 | WORKDIR /usr/src/sudo 4 | COPY target/build build 5 | # set setuid on install 6 | RUN install -m 4755 build/sudo /usr/local/bin/sudo && \ 7 | install -m 4755 build/su /usr/bin/su && \ 8 | install -m 755 build/visudo /usr/local/sbin/visudo 9 | # `apt-get install sudo` creates this directory; creating it in the image saves us the work of creating it in each compliance test 10 | RUN mkdir -p /usr/local/etc/sudoers.d 11 | # Ensure we use the same shell across OSes 12 | RUN chsh -s /bin/sh 13 | # set the default working directory to somewhere world writable so sudo / su can create .profraw files there 14 | WORKDIR /tmp 15 | # Makes sure our sudo implementation actually runs 16 | ENV SUDO_RS_IS_UNSTABLE="I accept that my system may break unexpectedly" 17 | -------------------------------------------------------------------------------- /test-framework/sudo-test/src/ours.freebsd.Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | # ignore everything 2 | * 3 | 4 | # but these 5 | !/target/build 6 | -------------------------------------------------------------------------------- /test-framework/sudo-test/src/ours.linux.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1-slim-bookworm 2 | RUN apt-get update && \ 3 | apt-get install -y --no-install-recommends apparmor libpam0g-dev libapparmor-dev procps sshpass rsyslog ca-certificates tzdata 4 | # cache the crates.io index in the image for faster local testing 5 | RUN cargo search sudo 6 | WORKDIR /usr/src/sudo 7 | COPY . . 8 | ARG SUDO_BUILD_FEATURES 9 | RUN --mount=type=cache,target=/usr/src/sudo/target cargo build --locked --features="$SUDO_BUILD_FEATURES" --bins && mkdir -p build && cp target/debug/sudo build/sudo && cp target/debug/su build/su && cp target/debug/visudo build/visudo 10 | # set setuid on install 11 | RUN install -m 4755 build/sudo /usr/bin/sudo && \ 12 | install -m 4755 build/su /usr/bin/su && \ 13 | install -m 755 build/visudo /usr/sbin/visudo 14 | # `apt-get install sudo` creates this directory; creating it in the image saves us the work of creating it in each compliance test 15 | RUN mkdir -p /etc/sudoers.d 16 | # Ensure we use the same shell across OSes 17 | RUN chsh -s /bin/sh 18 | # To ensure we can create a user with uid 1000 and to avoid having to use uid 1001 in test expectations 19 | RUN userdel ubuntu || true 20 | # set the default working directory to somewhere world writable so sudo / su can create .profraw files there 21 | WORKDIR /tmp 22 | # This env var needs to be set when compiled with the dev feature 23 | ENV SUDO_RS_IS_UNSTABLE="I accept that my system may break unexpectedly" 24 | -------------------------------------------------------------------------------- /test-framework/sudo-test/src/ours.linux.Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | # ignore everything 2 | * 3 | 4 | # but these 5 | !Cargo.lock 6 | !Cargo.toml 7 | !src/**/* 8 | !bin/**/* 9 | !build.rs 10 | -------------------------------------------------------------------------------- /test-framework/sudo-test/src/theirs.freebsd.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dougrabson/freebsd14.1-small:latest 2 | RUN IGNORE_OSVERSION=yes pkg install -y sudo pidof sshpass rsyslog bash vim dash FreeBSD-libbsm && \ 3 | rm /usr/local/etc/sudoers 4 | # Ensure we use the same shell across OSes 5 | RUN chsh -s /bin/sh 6 | # just to match `ours.Dockerfile` 7 | WORKDIR /tmp 8 | -------------------------------------------------------------------------------- /test-framework/sudo-test/src/theirs.linux.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | RUN apt-get update && \ 3 | apt-get install -y --no-install-recommends sudo procps sshpass rsyslog && \ 4 | rm /etc/sudoers 5 | # Ensure we use the same shell across OSes 6 | RUN chsh -s /bin/sh 7 | # To ensure we can create a user with uid 1000 and to avoid having to use uid 1001 in test expectations 8 | RUN userdel ubuntu || true 9 | # just to match `ours.Dockerfile` 10 | WORKDIR /tmp 11 | -------------------------------------------------------------------------------- /util/Dockerfile-release: -------------------------------------------------------------------------------- 1 | FROM rust:1.84-slim-bookworm@sha256:69fbd6ab81b514580bc14f35323fecb09feba9e74c5944ece9a70d9a2a369df0 2 | RUN apt-get update -y && apt-get install -y libpam0g-dev 3 | -------------------------------------------------------------------------------- /util/generate-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docs_dir="docs/man" 4 | files=("sudo.8" "visudo.8" "sudoers.5" "su.1") 5 | 6 | for f in "${files[@]}"; do 7 | origin_file="$docs_dir/$f.md" 8 | target_file="$docs_dir/$f.man" 9 | 10 | echo "Generating man page for $f from '$origin_file' to '$target_file'" 11 | util/pandoc.sh -s -t man "$origin_file" -o "$target_file" 12 | done 13 | -------------------------------------------------------------------------------- /util/pandoc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | exec docker run --rm -i -v "$(pwd):/data" -u "$(id -u):$(id -g)" "pandoc/core@sha256:668f5ced9d99ed0fd8b0efda93d6cead066565bb400fc1fb165e77ddbb586a16" "$@" 4 | -------------------------------------------------------------------------------- /util/update-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd) 4 | PROJECT_DIR=$(dirname "$SCRIPT_DIR") 5 | 6 | # Fetch current version 7 | CURRENT_VERSION=$(sed -n '/^version\s*=\s*"\([0-9.]*\)"/{s//\1/p;q}' "$PROJECT_DIR/Cargo.toml") 8 | 9 | # Fetch new version from changelog 10 | NEW_VERSION=$(grep -m1 '^##' "$PROJECT_DIR"/CHANGELOG.md | grep -o "\[[0-9]\+.[0-9]\+.[0-9]\+\]" | tr -d '[]') 11 | 12 | if [ -z "$NEW_VERSION" ]; then 13 | echo "Could not fetch version from CHANGELOG.md; you probably made a mistake." 14 | exit 1 15 | fi 16 | 17 | if [ "$CURRENT_VERSION" \> "$NEW_VERSION" ]; then 18 | echo "New version number must be higher than current version: $CURRENT_VERSION" 19 | echo "Create a new changelog entry before running this script!" 20 | exit 1 21 | fi 22 | 23 | if [ "$CURRENT_VERSION" == "$NEW_VERSION" ]; then 24 | echo "Cargo.toml was already at $NEW_VERSION" 25 | else 26 | echo "Updating version in Cargo.toml to $NEW_VERSION" 27 | sed -i 's/^version\s*=\s*".*"/version = "'"$NEW_VERSION"'"/' "$PROJECT_DIR/Cargo.toml" 28 | fi 29 | 30 | echo "Updating version in README.md installation instructions" 31 | sed -i 's/sudo-\(VERSION\|[0-9]\+\.[0-9]\+\.[0-9]\+\)\.tar\.gz/sudo-'"$NEW_VERSION"'\.tar\.gz/g' "$PROJECT_DIR/README.md" 32 | sed -i 's/su-\(VERSION\|[0-9]\+\.[0-9]\+\.[0-9]\+\)\.tar\.gz/su-'"$NEW_VERSION"'\.tar\.gz/g' "$PROJECT_DIR/README.md" 33 | 34 | echo "Updating version in man pages" 35 | sed -i 's/^title: SU(1) sudo-rs .*/title: SU(1) sudo-rs '"$NEW_VERSION"' | sudo-rs/' "$PROJECT_DIR"/docs/man/su.1.md 36 | sed -i 's/^title: SUDO(8) sudo-rs .*/title: SUDO(8) sudo-rs '"$NEW_VERSION"' | sudo-rs/' "$PROJECT_DIR"/docs/man/sudo.8.md 37 | sed -i 's/^title: VISUDO(8) sudo-rs .*/title: VISUDO(8) sudo-rs '"$NEW_VERSION"' | sudo-rs/' "$PROJECT_DIR"/docs/man/visudo.8.md 38 | 39 | echo "Rebuilding project" 40 | (cd $PROJECT_DIR && cargo build --release) 41 | --------------------------------------------------------------------------------