├── .github └── workflows │ ├── build-release.sh │ ├── build.sh │ ├── cli_test.sh │ ├── clippy.sh │ ├── comment.yml │ ├── doc.sh │ ├── format.sh │ ├── nightly.sh │ ├── restore-checks.sh │ ├── restore-test.sh │ ├── rust.yml │ └── test.sh ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── ROADMAP.md ├── benches └── cascade_benchmarks.rs ├── build.rs ├── clippy.toml ├── data ├── error_policies │ ├── alias.cas │ ├── alias_conflict.cas │ ├── bad_allow.cas │ ├── bad_inherits_ancestor_associated_call.cas │ ├── bad_inherits_associated_call.cas │ ├── bad_typecasts.cas │ ├── call_casting.cas │ ├── class_wrong.cas │ ├── cycle.cas │ ├── derive_diff_associated_call.cas │ ├── derive_explicit.cas │ ├── derive_noderive.cas │ ├── derive_non_matching_parents.cas │ ├── domain_filecon.cas │ ├── duplicate_alias.cas │ ├── duplicate_association.cas │ ├── duplicate_association_nested.cas │ ├── duplicate_inherit.cas │ ├── duplicate_inheritance.cas │ ├── error_in_associate.cas │ ├── extend_double_decl.cas │ ├── extend_no_decl.cas │ ├── fs_context.cas │ ├── fs_context_dup.cas │ ├── functions_arg_count.cas │ ├── functions_no_term.cas │ ├── functions_recursion.cas │ ├── initial_context.cas │ ├── invalid_default1.cas │ ├── invalid_default2.cas │ ├── let_invalid_type.cas │ ├── machine_invalid.cas │ ├── machine_invalid_module.cas │ ├── machine_missing_req_config.cas │ ├── machine_multiple_config.cas │ ├── machine_no_modules.cas │ ├── machine_virtual.cas │ ├── module_cycle.cas │ ├── module_invalid.cas │ ├── named_resource_trans.cas │ ├── networking_rules.cas │ ├── no_arg_function.cas │ ├── nonexistent_inheritance.cas │ ├── parse_unexpected_eof.cas │ ├── parse_unknown_token.cas │ ├── parse_unrecognized_token.cas │ ├── resource_trans.cas │ ├── self_function.cas │ ├── self_inherit.cas │ ├── self_subject.cas │ ├── trait_no_impl.cas │ ├── unsupplied_arg.cas │ ├── virtual_function_association.cas │ ├── virtual_function_illegal_call.cas │ └── virtual_function_non_define.cas ├── expected_cil │ ├── alias.cil │ ├── arg_call.cil │ ├── arguments.cil │ ├── associate.cil │ ├── attribute.cil │ ├── auditallow.cil │ ├── automatic_derive.cil │ ├── call_casting.cil │ ├── casting.cil │ ├── collection.cil │ ├── conditional.cil │ ├── default.cil │ ├── derive.cil │ ├── direct_association_reference.cil │ ├── domtrans.cil │ ├── dontaudit.cil │ ├── drop.cil │ ├── duplicate_inherit.cil │ ├── extend.cil │ ├── extend_inherit.cil │ ├── filecon.cil │ ├── fs_context.cil │ ├── function.cil │ ├── hint.cil │ ├── implicit_this.cil │ ├── inherit_alias.cil │ ├── initial_context.cil │ ├── let.cil │ ├── machines.cil │ ├── makelist.cil │ ├── module_alias.cil │ ├── module_arguments.cil │ ├── module_simple.cil │ ├── multifile1.cil │ ├── named_args.cil │ ├── named_resource_trans.cil │ ├── nested_alias.cil │ ├── nested_conflict.cil │ ├── networking_rules.cil │ ├── non_virtual_inherit.cil │ ├── nonexistent_optional.cil │ ├── optional.cil │ ├── permissions.cil │ ├── resource_trans.cil │ ├── self.cil │ ├── simple.cil │ ├── this_casting.cil │ ├── trait.cil │ └── virtual_function.cil └── policies │ ├── alias.cas │ ├── arg_call.cas │ ├── arguments.cas │ ├── associate.cas │ ├── attribute.cas │ ├── auditallow.cas │ ├── automatic_derive.cas │ ├── call_casting.cas │ ├── casting.cas │ ├── collection.cas │ ├── conditional.cas │ ├── default.cas │ ├── derive.cas │ ├── direct_association_reference.cas │ ├── domtrans.cas │ ├── dontaudit.cas │ ├── drop.cas │ ├── duplicate_inherit.cas │ ├── extend.cas │ ├── extend_inherit.cas │ ├── filecon.cas │ ├── fs_context.cas │ ├── full_system │ ├── README.md │ ├── all_files.cas │ ├── base_resource.cas │ ├── contexts │ │ ├── default_contexts │ │ ├── virtual_domain_context │ │ ├── virtual_image_context │ │ └── x_contexts │ ├── general.cas │ ├── kernel.cas │ ├── machine_config.cas │ └── unconfined.cas │ ├── function.cas │ ├── hint.cas │ ├── implicit_this.cas │ ├── inherit_alias.cas │ ├── initial_context.cas │ ├── let.cas │ ├── machine_building1.cas │ ├── machine_building2.cas │ ├── machine_building3.cas │ ├── machines.cas │ ├── makelist.cas │ ├── module_alias.cas │ ├── module_arguments.cas │ ├── module_simple.cas │ ├── multifile1.cas │ ├── multifile2.cas │ ├── named_args.cas │ ├── named_resource_trans.cas │ ├── nested_alias.cas │ ├── nested_conflict.cas │ ├── networking_rules.cas │ ├── non_virtual_inherit.cas │ ├── nonexistent_optional.cas │ ├── optional.cas │ ├── permissions.cas │ ├── resource_trans.cas │ ├── self.cas │ ├── simple.cas │ ├── stress │ └── functions.cas │ ├── this_casting.cas │ ├── tmp_file.cas │ ├── trait.cas │ └── virtual_function.cas ├── doc ├── TE.md ├── policy_composer.md └── resource_association.md ├── owners.txt ├── src ├── alias_map.rs ├── annotations.rs ├── ast.rs ├── bin │ ├── audit2cascade.rs │ └── casc │ │ ├── args.rs │ │ ├── main.rs │ │ └── package.rs ├── compile.rs ├── constants.rs ├── context.rs ├── dbus.rs ├── error.rs ├── functions.rs ├── internal_rep.rs ├── lib.rs ├── machine.rs ├── obj_class.rs ├── parser.lalrpop ├── sexp_internal.rs ├── test.rs ├── util.rs └── warning.rs └── tools └── update-expected-cil.sh /.github/workflows/build-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | 5 | rustup run stable cargo build --release --verbose 6 | -------------------------------------------------------------------------------- /.github/workflows/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | 5 | rustup run stable cargo build --verbose 6 | -------------------------------------------------------------------------------- /.github/workflows/cli_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | 5 | VERSION=${1:-stable} 6 | 7 | check_file () { 8 | if [[ ! -f "$1" ]] ; then 9 | echo "Failed to create $1" 10 | exit 1 11 | fi 12 | rm -f "$1" 13 | } 14 | 15 | # CLI tests 16 | rustup run ${VERSION} cargo build --verbose 17 | rustup run ${VERSION} cargo build --release --verbose 18 | 19 | # TODO add additional targets to tests when ready 20 | for d in debug release 21 | do 22 | ./target/$d/casc --help 23 | ./target/$d/casc -h 24 | 25 | ./target/$d/casc --version 26 | ./target/$d/casc -V 27 | 28 | ./target/$d/casc data/policies/simple.cas 29 | check_file out.cil 30 | 31 | ./target/$d/casc data/policies/simple.cas -o new.cil 32 | check_file new.cil 33 | 34 | ./target/$d/casc data/policies/simple.cas --color always 35 | check_file out.cil 36 | ./target/$d/casc data/policies/simple.cas --color auto 37 | check_file out.cil 38 | ./target/$d/casc data/policies/simple.cas --color never 39 | check_file out.cil 40 | done 41 | -------------------------------------------------------------------------------- /.github/workflows/clippy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | 5 | VERSION=${1:-stable} 6 | 7 | rustup run ${VERSION} cargo clippy -- --deny warnings 8 | 9 | rustup run ${VERSION} cargo clippy --tests 10 | -------------------------------------------------------------------------------- /.github/workflows/comment.yml: -------------------------------------------------------------------------------- 1 | name: Comment on clippy 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Rust"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | download: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | # We lock our failing tests on a known good version and 15 | # informationally check the latest as well. We should 16 | # regularly update the known good once we know that tests 17 | # pass on it 18 | # This needs to stay in sync with rust.yml minus the known 19 | # good clippy version 20 | rust-toolchain: [ stable, nightly ] 21 | if: > 22 | github.event.workflow_run.event == 'pull_request' && 23 | github.event.workflow_run.conclusion == 'success' 24 | steps: 25 | - name: 'Download clippy artifact' 26 | uses: actions/github-script@v6 27 | with: 28 | script: | 29 | var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 30 | owner: context.repo.owner, 31 | repo: context.repo.repo, 32 | run_id: ${{github.event.workflow_run.id }}, 33 | }); 34 | var matchArtifact = artifacts.data.artifacts.filter((artifact) => { 35 | return artifact.name == "clippy-${{ matrix.rust-toolchain }}" 36 | })[0]; 37 | var download = await github.rest.actions.downloadArtifact({ 38 | owner: context.repo.owner, 39 | repo: context.repo.repo, 40 | artifact_id: matchArtifact.id, 41 | archive_format: 'zip', 42 | }); 43 | var fs = require('fs'); 44 | fs.writeFileSync('${{github.workspace}}/clippy-${{ matrix.rust-toolchain }}.zip', Buffer.from(download.data)); 45 | - name: 'Download test-nightly artifact' 46 | uses: actions/github-script@v6 47 | if: (matrix.rust-toolchain == 'nightly') 48 | with: 49 | script: | 50 | var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 51 | owner: context.repo.owner, 52 | repo: context.repo.repo, 53 | run_id: ${{github.event.workflow_run.id }}, 54 | }); 55 | var matchArtifact = artifacts.data.artifacts.filter((artifact) => { 56 | return artifact.name == "test-${{ matrix.rust-toolchain }}" 57 | })[0]; 58 | var download = await github.rest.actions.downloadArtifact({ 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | artifact_id: matchArtifact.id, 62 | archive_format: 'zip', 63 | }); 64 | var fs = require('fs'); 65 | fs.writeFileSync('${{github.workspace}}/test-${{ matrix.rust-toolchain }}.zip', Buffer.from(download.data)); 66 | - run: unzip clippy-${{ matrix.rust-toolchain }}.zip 67 | - if: (matrix.rust-toolchain == 'nightly') 68 | run: unzip test-${{ matrix.rust-toolchain }}.zip 69 | - name: Notify clippy failure 70 | uses: actions/github-script@v6 71 | with: 72 | github-token: ${{ secrets.GITHUB_TOKEN }} 73 | script: | 74 | var fs = require('fs'); 75 | var clippy_ret = Number(fs.readFileSync('./flag')); 76 | var issue_number = Number(fs.readFileSync('./issue_num')); 77 | if (clippy_ret != 0) { 78 | github.rest.issues.createComment({ 79 | issue_number: issue_number, 80 | owner: context.repo.owner, 81 | repo: context.repo.repo, 82 | body: 'Stable clippy has failed for this run: ${{ matrix.rust-toolchain }}. A maintainer should check the logs. If known good clippy passed, this is non-fatal to the PR.' 83 | }) 84 | } 85 | - name: Notify nightly test failure 86 | uses: actions/github-script@v6 87 | if: (matrix.rust-toolchain == 'nightly') 88 | with: 89 | github-token: ${{ secrets.GITHUB_TOKEN }} 90 | script: | 91 | var fs = require('fs'); 92 | var nightly_ret = Number(fs.readFileSync('./nightly_flag')); 93 | var issue_number = Number(fs.readFileSync('./issue_num')); 94 | if (nightly_ret != 0) { 95 | github.rest.issues.createComment({ 96 | issue_number: issue_number, 97 | owner: context.repo.owner, 98 | repo: context.repo.repo, 99 | body: 'Nightly test has failed for this run. A maintainer should check the logs. If known good clippy passed, this is non-fatal to the PR.' 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /.github/workflows/doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | 5 | VERSION=${1:-stable} 6 | 7 | rustup run ${VERSION} cargo doc --no-deps 8 | -------------------------------------------------------------------------------- /.github/workflows/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | 5 | VERSION=${1:-stable} 6 | 7 | rustup run ${VERSION} cargo fmt --all -- --check 8 | -------------------------------------------------------------------------------- /.github/workflows/nightly.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | 5 | # Gently stops when an error occurs. 6 | rustup run nightly cargo build --verbose || exit 0 7 | rustup run nightly cargo test --verbose || exit 0 8 | rustup run nightly cargo doc --no-deps || exit 0 9 | rustup run nightly cargo fmt --all -- --check || exit 0 10 | -------------------------------------------------------------------------------- /.github/workflows/restore-checks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | 5 | VERSION=${1:-stable} 6 | 7 | # https://nickb.dev/blog/azure-pipelines-for-rust-projects 8 | curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y 9 | export PATH="${PATH}:${HOME}/.cargo/bin" 10 | echo "##vso[task.setvariable variable=PATH;]${PATH}" 11 | 12 | rustup toolchain install ${VERSION} 13 | rustup component add rustfmt --toolchain ${VERSION} 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/restore-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -x -o pipefail 4 | 5 | VERSION=${1:-stable} 6 | 7 | # Manually build secilc, rather than take the packaged version, so we can have control over version 8 | # Github actions will have already checked out the repo to the correct tag for this run 9 | sudo apt update 10 | sudo apt-get install --no-install-recommends --no-install-suggests \ 11 | bison \ 12 | flex \ 13 | gawk \ 14 | gcc \ 15 | gettext \ 16 | make \ 17 | libaudit-dev \ 18 | libbz2-dev \ 19 | libcap-dev \ 20 | libcap-ng-dev \ 21 | libcunit1-dev \ 22 | libglib2.0-dev \ 23 | libpcre2-dev \ 24 | libpcre3-dev \ 25 | pkgconf \ 26 | python3 \ 27 | systemd \ 28 | xmlto 29 | 30 | pushd selinux 31 | 32 | # 3.2 and earlier have a warning for stringop-truncation 33 | # 3.0 and earlier have multiple definitions of global variables, which fails to 34 | # compile with -fno-common, which is the default behavior in modern GCC. This 35 | # was fixed upstream in commit a96e8c59ecac84096d870b42701a504791a8cc8c, but 36 | # for our purposes compiling the older versions, we can just allow the behavior 37 | # with -fcommon 38 | sudo make LIBDIR=/usr/local/lib/x86_64-linux-gnu SHLIBDIR=/lib/x86_64-linux-gnu CFLAGS="-Wno-error=stringop-truncation -fcommon -pipe -fPIC" OPT_SUBDIRS="" install 39 | 40 | # https://nickb.dev/blog/azure-pipelines-for-rust-projects 41 | curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y 42 | export PATH="${PATH}:${HOME}/.cargo/bin" 43 | echo "##vso[task.setvariable variable=PATH;]${PATH}" 44 | 45 | rustup toolchain install ${VERSION} 46 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: .github/workflows/build.sh 21 | checks: 22 | runs-on: ubuntu-latest 23 | needs: build 24 | strategy: 25 | matrix: 26 | # We lock our failing tests on a known good version and 27 | # informationally check the latest as well. We should 28 | # regularly update the known good once we know that tests 29 | # pass on it 30 | rust-toolchain: [ 1.83, stable, nightly ] 31 | steps: 32 | - uses: actions/checkout@v3 33 | - name: Setup 34 | run: .github/workflows/restore-checks.sh ${{ matrix.rust-toolchain }} 35 | - name: Check format 36 | run: .github/workflows/format.sh ${{ matrix.rust-toolchain }} 37 | - name: Check clippy 38 | id: clippy 39 | run: .github/workflows/clippy.sh ${{ matrix.rust-toolchain }} 40 | continue-on-error: ${{ matrix.rust-toolchain != '1.83' }} 41 | - name: Store clippy flag 42 | if: (matrix.rust-toolchain != '1.83') 43 | run: | 44 | mkdir -p ./clippy-${{ matrix.rust-toolchain }} 45 | if [[ ${{ steps.clippy.outcome }} == "success" ]] ; then \ 46 | echo 0 > ./clippy-${{ matrix.rust-toolchain }}/flag; \ 47 | else \ 48 | echo 1 > ./clippy-${{ matrix.rust-toolchain }}/flag; \ 49 | fi 50 | echo ${{ github.event.number }} > ./clippy-${{ matrix.rust-toolchain }}/issue_num 51 | - uses: actions/upload-artifact@v4 52 | if: (matrix.rust-toolchain != '1.83') 53 | with: 54 | name: clippy-${{ matrix.rust-toolchain }} 55 | path: clippy-${{ matrix.rust-toolchain }}/ 56 | - name: Check docs 57 | run: .github/workflows/doc.sh ${{ matrix.rust-toolchain }} 58 | - name: Build release 59 | run: .github/workflows/build-release.sh 60 | test: 61 | runs-on: ubuntu-latest 62 | needs: build 63 | strategy: 64 | fail-fast: false 65 | matrix: 66 | # 2.7 is the version in Ubuntu 18.04 67 | # 3.0 is the version in Ubuntu 20.04 68 | # 2.9 and earlier do not yet support policy.32 and 69 | # we default to policy.32 for now. Once we support 70 | # configurable policy versions, we should be able to successfully 71 | # test against 2.7-2.9 72 | 73 | selinux-version: [ secilc-3.0, secilc-3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8 ] 74 | rust-toolchain: [ stable ] 75 | include: 76 | - selinux-version: '3.7' 77 | rust-toolchain: nightly 78 | - selinux-version: '3.7' 79 | rust-toolchain: 1.81 80 | steps: 81 | - uses: actions/checkout@v3 82 | - name: Checkout selinux userspace 83 | uses: actions/checkout@v3 84 | with: 85 | repository: SELinuxProject/selinux 86 | ref: refs/tags/${{ matrix.selinux-version }} 87 | path: selinux 88 | - name: Setup 89 | run: .github/workflows/restore-test.sh ${{ matrix.rust-toolchain }} 90 | - name: Run tests 91 | id: test 92 | run: .github/workflows/test.sh ${{ matrix.rust-toolchain }} 93 | continue-on-error: ${{ matrix.rust-toolchain == 'nightly' }} 94 | - name: Store nightly flag 95 | if: (matrix.rust-toolchain == 'nightly') 96 | run: | 97 | mkdir -p ./test-${{ matrix.rust-toolchain }} 98 | if [[ ${{ steps.test.outcome }} == "success" ]] ; then \ 99 | echo 0 > ./test-${{ matrix.rust-toolchain }}/nightly_flag; \ 100 | else \ 101 | echo 1 > ./test-${{ matrix.rust-toolchain }}/nightly_flag; \ 102 | fi 103 | # Note, unlike above we are not storing issue_num in this artifact. 104 | # That is because it will be stored in the clippy artifact and both 105 | # get unzipped if the toolchain is nightly. 106 | - uses: actions/upload-artifact@v4 107 | if: (matrix.rust-toolchain == 'nightly') 108 | with: 109 | name: test-${{ matrix.rust-toolchain }} 110 | path: test-${{ matrix.rust-toolchain }}/ 111 | 112 | -------------------------------------------------------------------------------- /.github/workflows/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -x -o pipefail 4 | 5 | VERSION=${1:-stable} 6 | 7 | rustup run ${VERSION} cargo test --verbose 8 | 9 | ./.github/workflows/cli_test.sh ${VERSION} 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *_test_out_policy 2 | /*.cil 3 | /target 4 | Cargo.lock 5 | file_contexts 6 | out.cil 7 | policy.* 8 | selinux_policy.tar.gz 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Cascade changelog 2 | 3 | ## [0.0.2] 4 | * Improve command line functionality 5 | * Let bindings 6 | * Compile/parse error recovery 7 | * Named function arguments 8 | * Aliases 9 | * Virtual functions 10 | * Default arguments 11 | * Better traceability on internal errors 12 | * Bug fixes 13 | * Documentation cleanup and enhancement 14 | 15 | ## [0.0.1] 16 | Initial Release 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "selinux-cascade" 3 | version = "0.0.2" 4 | description = "A High Level Language for specifying SELinux policy" 5 | authors = ["Daniel Burgener ", "Mickael Salaun "] 6 | edition = "2018" 7 | license = "MIT" 8 | repository = "https://github.com/dburgener/cascade" 9 | readme = "README.md" 10 | keywords = [ "selinux" ] 11 | rust-version = "1.81" 12 | 13 | [build-dependencies] 14 | lalrpop = { version="0.22", default-features=false, features = ["lexer"] } 15 | clap = { version = "4", features = ["derive"] } 16 | clap_mangen = "0.2" 17 | 18 | [dev-dependencies] 19 | criterion = "0.5" 20 | 21 | [dependencies] 22 | backtrace = "0.3" 23 | clap = { version = "4", features = ["derive"] } 24 | codespan-reporting = "0.11" 25 | flate2 = "1" 26 | is-terminal = "0.4" 27 | lalrpop-util = { version="0.22", default-features=false, features = ["lexer"] } 28 | quick-xml = "0.37" 29 | sexp = "1.1" 30 | tar = "0.4" 31 | termcolor = "1.1" 32 | thiserror = "2.0" 33 | walkdir = "2" 34 | 35 | [[bench]] 36 | name = "cascade_benchmarks" 37 | harness = false 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | Cascade is a project to build a new high level language for defining SELinux 3 | policy. 4 | 5 | The overall structure of the language is essentially object oriented, with types 6 | carrying knowledge of their use and a hierarchical inheritance tree of type 7 | definition which reflects real world usage in a variety of scenarios. The 8 | syntax is largely rust inspired, although inspiriation is taken from a variety 9 | of language with a focus on simplicity, consistency and familiarity to 10 | developers from a variety of backgrounds. 11 | 12 | # Getting Started 13 | To build the executables run: 14 | 15 | ``` 16 | $ cargo build 17 | ``` 18 | 19 | To run tests, run: 20 | 21 | ``` 22 | $ cargo test 23 | ``` 24 | 25 | Cargo will automatically download all Rust crate dependencies. The tests depend 26 | on the secilc package. 27 | 28 | ## casc 29 | The Cascade compiler is named casc, and will be located at target/debug/casc after a 30 | successful build. Input files are supplied as arguments. Directory arguments 31 | are searched recursively for policy files. If no valid policy files are found, 32 | casc will exit with an error. 33 | 34 | ``` 35 | $ casc my_policy.cas 36 | ``` 37 | 38 | casc will create a file named out.cil, containing CIL policy. This CIL policy 39 | can then be compiled into final SELinux policy using secilc. 40 | 41 | More arguments and configuration for casc will be added in future releases 42 | 43 | ## audit2cascade 44 | The current audit2cascade binary is a simple placeholder. Eventually this will 45 | be turned into a tool similar to audit2allow or audit2why which generates 46 | Cascade policy based on an output of AVC denial messages in the audit logs. It 47 | will take advantage of the semantic information present in the hll policy to 48 | aid the developer in making intelligent decisions about handling denials rather 49 | than simply adding raw allow rules. 50 | 51 | # Writing Cascade policy 52 | For details on writing Cascade policy, see [Type Enforcement](doc/TE.md). 53 | 54 | # Contribute 55 | Thank you for your interest in contributing! There are several ways you can 56 | contribute to this project. 57 | 58 | ## Reporting bugs and suggesting enhancements 59 | If you see something wrong or have a suggestion for improvement, feel free to 60 | create an issue in the [Issue tracker](https://github.com/dburgener/cascade/issues) 61 | 62 | ## Contributing code 63 | We'd welcome your code contributions via GitHub PR. If you're planning on 64 | adding a major feature, it would probably be good to discuss it in the issue 65 | tracker prior to doing too much work so that we can all come to a consensus on 66 | how it should work. No advanced discussion is needed for smaller tweaks and 67 | bug fixes. 68 | 69 | ## Project status 70 | The project is still in its early stages and is being developed and improved 71 | rapidly. Not all features present in the documentation may be fully 72 | implemented yet. For the latest changes please see [CHANGELOG.md](CHANGELOG.md), and for 73 | future work plans and milestones please see [ROADMAP.md](ROADMAP.md). 74 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | This document describes the project roadmap and goals. It is intended to be a living document, and open to community input. 3 | 4 | ## Major milestones 5 | The details of what is included in these milestones is described below. 6 | 7 | 0.1 (targeted by 2023-03-31) - Clean compilation of a substantial system policy 8 | 9 | 1.0 (targeted by 2023-09-30) - Capable of building a functional TE policy for booting a Fedora 36 system in enforcing mode with comparable functionality to targeted policy 10 | 11 | 1.1 (targeted by 2023-09-30) - audit2cascade 12 | 13 | 1.2 (targeted TBD) - UBAC and RBAC 14 | 15 | # Detailed roadmap steps 16 | This lists remaining steps. Remove steps below as they are completed. 17 | 18 | General bugfixing and clean-up tasks are assumed. This lists major features needed for each milestone. 19 | 20 | ## 0.1 21 | * Associate resources with resources 22 | * Associate types via nesting in a block 23 | * Syntax to call parent class version of functions 24 | 25 | ## 1.0 26 | * Documentation Comments 27 | * Port labeling implementation 28 | * Conditionals - Combining both tunables and booleans into a single feature with configurability for runtime vs compiletime resolution 29 | * Documentation updates 30 | 31 | ## 1.1 32 | * Add support for debug symbols carried with the policy 33 | * Implement @hint annotation 34 | * audit2cascade front end 35 | * audit2cascade back end structure - A flexible engine for combining heuristics about policy to make recommendations 36 | * heuristic #1 - TBD 37 | * heuristic #2 - TBD 38 | * heuristic #3 - TBD 39 | * Build individual policy modules and systems 40 | * Enhanced file_context path support - Treat paths as though they are actual paths, rather than just strings/regexes 41 | 42 | ## 1.2 43 | * RBAC support 44 | * UBAC support 45 | 46 | ## 1.3 47 | * neverallow rules 48 | * additional higher level abstractions - TBD 49 | * Automatically check documentation examples for correctness 50 | 51 | ## Future 52 | * MLS/MCS support 53 | * Policy binary size optimizations 54 | * Object class and permission customization 55 | * Generics (possibly?) 56 | -------------------------------------------------------------------------------- /benches/cascade_benchmarks.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; 2 | 3 | use walkdir::WalkDir; 4 | 5 | use selinux_cascade::compile_combined; 6 | 7 | pub fn full_system(c: &mut Criterion) { 8 | let full_system_path = "data/policies/full_system"; 9 | let mut policy_files = Vec::new(); 10 | 11 | for entry in WalkDir::new(full_system_path) { 12 | let entry = entry.unwrap(); 13 | if entry.file_type().is_file() && entry.path().extension().unwrap_or_default() == "cas" { 14 | policy_files.push(entry.path().display().to_string()); 15 | } 16 | } 17 | 18 | let policy_files: Vec<&str> = policy_files.iter().map(|s| s as &str).collect(); 19 | 20 | c.bench_function("Full system compile", |b| { 21 | b.iter_batched( 22 | || policy_files.clone(), 23 | |policy_files| compile_combined(black_box(policy_files)), 24 | BatchSize::SmallInput, 25 | ) 26 | }); 27 | } 28 | 29 | pub fn stress_functions(c: &mut Criterion) { 30 | let policy_files = vec!["data/policies/stress/functions.cas"]; 31 | c.bench_function("Stress functions", |b| { 32 | b.iter_batched( 33 | || policy_files.clone(), 34 | |policy_files| compile_combined(black_box(policy_files)), 35 | BatchSize::SmallInput, 36 | ) 37 | }); 38 | } 39 | 40 | criterion_group!(benches, full_system, stress_functions); 41 | criterion_main!(benches); 42 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate lalrpop; 2 | 3 | use clap::CommandFactory; 4 | 5 | #[path = "src/bin/casc/args.rs"] 6 | mod casc; 7 | 8 | fn main() -> std::io::Result<()> { 9 | // Generate parser 10 | lalrpop::process_src().unwrap(); 11 | 12 | // Generate man page 13 | // https://rust-cli.github.io/book/in-depth/docs.html 14 | let out_dir = 15 | std::path::PathBuf::from(std::env::var_os("OUT_DIR").ok_or(std::io::ErrorKind::NotFound)?); 16 | let cmd = casc::Args::command(); 17 | let man = clap_mangen::Man::new(cmd); 18 | let mut buffer: Vec = Default::default(); 19 | man.render(&mut buffer)?; 20 | 21 | std::fs::write(out_dir.join("casc.1"), buffer)?; 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | large-error-threshold = 256 2 | -------------------------------------------------------------------------------- /data/error_policies/alias.cas: -------------------------------------------------------------------------------- 1 | @alias("foo") 2 | resource bar {} 3 | 4 | @alias([foo]) 5 | resource baz {} 6 | -------------------------------------------------------------------------------- /data/error_policies/alias_conflict.cas: -------------------------------------------------------------------------------- 1 | virtual resource parent { 2 | fn func(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | } 6 | 7 | virtual resource foo inherits parent {} 8 | 9 | @alias(foo) 10 | virtual resource bar inherits parent {} 11 | 12 | domain dom { 13 | allow(this, foo, file, read); 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /data/error_policies/bad_allow.cas: -------------------------------------------------------------------------------- 1 | resource bar {} 2 | 3 | domain foo { 4 | allow(this); 5 | allow(this, bar, file, bad_perm); 6 | allow(this, this, capability, [ read write ]); 7 | allow(this, this, capability2, [ read ]); 8 | } 9 | -------------------------------------------------------------------------------- /data/error_policies/bad_inherits_ancestor_associated_call.cas: -------------------------------------------------------------------------------- 1 | virtual domain userdomain {} 2 | 3 | virtual resource ancestor_user_tmp { 4 | @associated_call 5 | fn associated_call_from_user_tmp(userdomain source) { 6 | allow(source, user_tmp, file, [read]); 7 | } 8 | } 9 | 10 | virtual resource user_tmp inherits ancestor_user_tmp {} 11 | 12 | resource real_user_tmp inherits user_tmp {} 13 | 14 | @associate([real_user_tmp]) 15 | domain bad_userdomain { 16 | // Errors because bad_userdomain does not inherit userdomain, which 17 | // the associated call above requires 18 | } -------------------------------------------------------------------------------- /data/error_policies/bad_inherits_associated_call.cas: -------------------------------------------------------------------------------- 1 | virtual domain userdomain {} 2 | 3 | virtual resource user_tmp { 4 | @associated_call 5 | fn associated_call_from_user_tmp(userdomain source) { 6 | allow(source, user_tmp, file, [read]); 7 | } 8 | } 9 | 10 | @associate([user_tmp]) 11 | domain bad_userdomain { 12 | // Errors because bad_userdomain does not inherit userdomain, which 13 | // the associated call above requires 14 | } 15 | 16 | domain dom { 17 | resource res { 18 | @associated_call 19 | fn call(userdomain source) { 20 | allow(source, this, file, read); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /data/error_policies/bad_typecasts.cas: -------------------------------------------------------------------------------- 1 | resource foo {} 2 | 3 | domain bar { 4 | allow(bar, "some string", file, read); 5 | allow(bar, foo, file, read); 6 | allow(bar, file, file, read); 7 | allow(bar, foo, file, read); 8 | } 9 | -------------------------------------------------------------------------------- /data/error_policies/call_casting.cas: -------------------------------------------------------------------------------- 1 | virtual resource bar { 2 | fn read(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | } 6 | 7 | virtual resource zap { 8 | fn write(domain source) { 9 | allow(source, this, file, write); 10 | } 11 | } 12 | 13 | resource foo inherits bar { 14 | fn read(domain source) { 15 | allow(source, this, dir, read); 16 | } 17 | } 18 | 19 | domain dom { 20 | foo.read(this); // Function does not exist 21 | foo.read(this); // Function does not exist 22 | foo.write(this); // Function does not exist 23 | } 24 | 25 | domain asd { 26 | asd.read_boo_tmp(this); // Function is not castable 27 | asd.read_boo_tmp_again(this); // Function is not castable 28 | asd.read_boo_tmp_more(this); // Function is not castable 29 | asd.some_function(foo); 30 | } 31 | 32 | virtual resource tmp { 33 | @associated_call 34 | fn associated_call_from_tmp(domain source) { 35 | allow(source, tmp, file, [read]); 36 | } 37 | } 38 | 39 | @associate([tmp]) 40 | domain boo { 41 | // Creates new resources boo.tmp and implicitly calls 42 | // boo.tmp.associated_call_from_tmp(boo) 43 | // 44 | // boo.tmp inherits tmp 45 | 46 | fn read_boo_tmp(domain source) { 47 | allow(source, boo.tmp, file, [read]); 48 | } 49 | 50 | fn read_boo_tmp_again(domain source) { 51 | boo.tmp.associated_call_from_tmp(source); 52 | } 53 | 54 | fn read_boo_tmp_more(domain source) { 55 | this.some_function(this.tmp); 56 | } 57 | 58 | fn some_function(resource res) { 59 | allow(this, res, file, read); 60 | } 61 | } -------------------------------------------------------------------------------- /data/error_policies/class_wrong.cas: -------------------------------------------------------------------------------- 1 | domain dom { 2 | allow(this, self, capability, [cap2_userns]); 3 | } 4 | -------------------------------------------------------------------------------- /data/error_policies/cycle.cas: -------------------------------------------------------------------------------- 1 | virtual domain foo inherits bar {} 2 | 3 | virtual domain bar inherits foo {} 4 | -------------------------------------------------------------------------------- /data/error_policies/derive_diff_associated_call.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo { 2 | @associated_call 3 | fn some_associated_call(domain source) { 4 | allow(source, this, file, link); 5 | } 6 | } 7 | 8 | virtual resource bar { 9 | fn some_associated_call(domain source) { 10 | allow(source, this, dir, add_name); 11 | } 12 | } 13 | 14 | @derive([some_associated_call], parents=*) 15 | virtual resource to_associate inherits foo, bar {} 16 | 17 | @associate([to_associate]) 18 | domain associates {} 19 | -------------------------------------------------------------------------------- /data/error_policies/derive_explicit.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo { 2 | fn read(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | } 6 | 7 | @derive([read], [foo]) 8 | resource bar inherits foo { 9 | fn read(domain source) { 10 | allow(source, this, file, write); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /data/error_policies/derive_noderive.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo {} 2 | 3 | @noderive 4 | @derive(*,*) 5 | resource bar inherits foo {} 6 | 7 | domain dom { 8 | allow(this, bar, file, read); 9 | } 10 | -------------------------------------------------------------------------------- /data/error_policies/derive_non_matching_parents.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo { 2 | fn do_something(domain source, domain target) { 3 | allow(source, target, file, read); 4 | } 5 | } 6 | 7 | virtual resource bar { 8 | fn do_something(domain source) { 9 | allow(source, this, file, read); 10 | } 11 | } 12 | 13 | @derive([do_something], *) 14 | resource baz inherits foo, bar {} 15 | -------------------------------------------------------------------------------- /data/error_policies/domain_filecon.cas: -------------------------------------------------------------------------------- 1 | domain foo { 2 | file_context("/bin", [file dir]); 3 | } 4 | -------------------------------------------------------------------------------- /data/error_policies/duplicate_alias.cas: -------------------------------------------------------------------------------- 1 | @alias(some_alias) 2 | domain foo { 3 | allow(this, resource, file, read); 4 | } 5 | 6 | @alias(some_alias) 7 | domain bar {} 8 | -------------------------------------------------------------------------------- /data/error_policies/duplicate_association.cas: -------------------------------------------------------------------------------- 1 | virtual resource tmp {} 2 | 3 | @associate([tmp]) 4 | virtual domain bar {} 5 | 6 | @associate([tmp]) 7 | domain foo inherits bar { 8 | allow(this, this.tmp, file, write); 9 | } 10 | -------------------------------------------------------------------------------- /data/error_policies/duplicate_association_nested.cas: -------------------------------------------------------------------------------- 1 | virtual resource tmp {} 2 | 3 | @associate([tmp]) 4 | virtual domain bar {} 5 | 6 | domain baz inherits bar { 7 | resource tmp {} 8 | } 9 | 10 | -------------------------------------------------------------------------------- /data/error_policies/duplicate_inherit.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo {} 2 | 3 | virtual resource bar {} 4 | 5 | resource baz inherits foo, foo, bar {} 6 | 7 | resource zap inherits foo, bar, foo {} 8 | 9 | domain requires_allow { 10 | allow(this, baz, file, read); 11 | } -------------------------------------------------------------------------------- /data/error_policies/duplicate_inheritance.cas: -------------------------------------------------------------------------------- 1 | virtual domain foo {} 2 | 3 | resource some_resource {} 4 | 5 | domain baz inherits foo, foo { 6 | allow(this, some_resource, file, read); 7 | } 8 | -------------------------------------------------------------------------------- /data/error_policies/error_in_associate.cas: -------------------------------------------------------------------------------- 1 | domain dom { 2 | resource res { 3 | file_context("/too/few/args"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /data/error_policies/extend_double_decl.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn my_func(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | } 6 | 7 | extend foo { 8 | // Illegal double-declaration 9 | fn my_func(domain source) { 10 | allow(source, this, file, write); 11 | } 12 | } 13 | 14 | domain bar { 15 | foo.my_func(this); 16 | } 17 | -------------------------------------------------------------------------------- /data/error_policies/extend_no_decl.cas: -------------------------------------------------------------------------------- 1 | extend foo {} 2 | -------------------------------------------------------------------------------- /data/error_policies/fs_context.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fs_context("ext3", xattr, bob); 3 | fs_context("sockfs", fs_type, this); 4 | fs_context("sockfs", foo, this); 5 | fs_context("proc", zap, this); 6 | 7 | fs_context("sysfs", genfscon, this, "/zap", [bar]); 8 | fs_context("sysfs", genfscon, this, "/zap", [file bar]); 9 | fs_context("fs1", xattr, this, "/zap", [file dir]); 10 | fs_context("fs2", task, this, "/zap"); 11 | 12 | // Policies must include at least one av rule 13 | allow(domain, foo, file, [read]); 14 | } 15 | -------------------------------------------------------------------------------- /data/error_policies/fs_context_dup.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fs_context("ext3", xattr, foo); 3 | fs_context("ext3", task, foo); 4 | fs_context("ext3", trans, foo); 5 | 6 | fs_context("sysfs", genfscon, this, "/zap", [dir]); 7 | fs_context("sysfs", genfscon, this, "/zap", [file]); 8 | fs_context("sysfs", genfscon, this, "/zap", [any]); 9 | fs_context("sysfs", genfscon, this, "/zap"); 10 | 11 | fs_context("test", genfscon, this, "/zap/baa", [file]); 12 | 13 | // Policies must include at least one av rule 14 | allow(domain, foo, file, [read]); 15 | } 16 | 17 | resource bar { 18 | fs_context("test", genfscon, this, "/zap/baa", [file]); 19 | } 20 | 21 | resource xyz { 22 | fs_context("test", genfscon, this, "/zap/baa", [file]); 23 | } 24 | -------------------------------------------------------------------------------- /data/error_policies/functions_arg_count.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn some_func(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | 6 | fn two_arg_func(domain source, domain other) { 7 | allow(source, this, file, write); 8 | } 9 | } 10 | 11 | domain bar { 12 | foo.some_func(this, "a bad arg"); 13 | foo.two_arg_func(this); 14 | foo.two_arg_func(this, this, this); 15 | } 16 | -------------------------------------------------------------------------------- /data/error_policies/functions_no_term.cas: -------------------------------------------------------------------------------- 1 | resource my_file { 2 | fn read(domain source) { 3 | allow(source, this, file, [ read open getattr ]); 4 | other_read(source); 5 | } 6 | 7 | fn other_read(domain source) { 8 | allow(source, this, file, [ read open getattr ]); 9 | third_read(source); 10 | } 11 | 12 | fn third_read(domain source) { 13 | allow(source, this, file, [ read open getattr ]); 14 | read(source); 15 | } 16 | } 17 | 18 | domain my_domain { 19 | my_file.read(this); // TODO: support 'this' as default argument 20 | } 21 | -------------------------------------------------------------------------------- /data/error_policies/functions_recursion.cas: -------------------------------------------------------------------------------- 1 | resource my_file { 2 | fn read(domain source) { 3 | allow(source, this, file, [ read open getattr ]); 4 | other_read(source); 5 | } 6 | 7 | fn other_read(domain source) { 8 | allow(source, this, file, [ read open getattr ]); 9 | third_read(source); 10 | } 11 | 12 | fn third_read(domain source) { 13 | allow(source, this, file, [ read open getattr ]); 14 | read(source); 15 | } 16 | 17 | fn term_read(domain source) { 18 | if (true) { 19 | allow(source, this, file, [ read open getattr ]); 20 | } 21 | optional { 22 | allow(source, this, dir, [ search ]); 23 | } 24 | } 25 | } 26 | 27 | domain my_domain { 28 | my_file.read(this); // TODO: support 'this' as default argument 29 | } 30 | -------------------------------------------------------------------------------- /data/error_policies/initial_context.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn create_initial_context(resource context) { 3 | initial_context("unlabeled", context); // illegal 4 | } 5 | } 6 | 7 | domain d { 8 | allow(this, foo, file, read); 9 | } 10 | -------------------------------------------------------------------------------- /data/error_policies/invalid_default1.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn some_func(domain source, resource other=invalid_symbol) { 3 | allow(source, other, file, read); 4 | } 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/error_policies/invalid_default2.cas: -------------------------------------------------------------------------------- 1 | domain dom { 2 | foo.some_func(this); 3 | allow(this, foo, file, write); 4 | } 5 | -------------------------------------------------------------------------------- /data/error_policies/let_invalid_type.cas: -------------------------------------------------------------------------------- 1 | let bar = baz; 2 | 3 | resource foo {} 4 | 5 | domain qux { 6 | allow(this, foo, file, read); 7 | } 8 | -------------------------------------------------------------------------------- /data/error_policies/machine_invalid.cas: -------------------------------------------------------------------------------- 1 | machine name { 2 | module module_name; 3 | let machine_type = default; 4 | let monolithic = truee; 5 | let config = option; 6 | } 7 | -------------------------------------------------------------------------------- /data/error_policies/machine_invalid_module.cas: -------------------------------------------------------------------------------- 1 | machine name { 2 | module foo; 3 | module bar; 4 | let handle_unknown_perms = deny; 5 | } 6 | 7 | module foo {} 8 | -------------------------------------------------------------------------------- /data/error_policies/machine_missing_req_config.cas: -------------------------------------------------------------------------------- 1 | machine name { 2 | module mod; 3 | let monolithic = true; 4 | let machine_type = standard; 5 | } 6 | 7 | module mod {} 8 | -------------------------------------------------------------------------------- /data/error_policies/machine_multiple_config.cas: -------------------------------------------------------------------------------- 1 | machine name { 2 | module mod; 3 | let monolithic = true; 4 | let handle_unknown_perms = allow; 5 | let monolithic = true; 6 | } 7 | 8 | module mod {} 9 | -------------------------------------------------------------------------------- /data/error_policies/machine_no_modules.cas: -------------------------------------------------------------------------------- 1 | machine name { 2 | let machine_type = standard; 3 | let handle_unknown_perms = allow; 4 | } 5 | -------------------------------------------------------------------------------- /data/error_policies/machine_virtual.cas: -------------------------------------------------------------------------------- 1 | virtual machine name { 2 | module mod; 3 | let machine_type = standard; 4 | let handle_unknown_perms = allow; 5 | } 6 | 7 | module mod {} 8 | -------------------------------------------------------------------------------- /data/error_policies/module_cycle.cas: -------------------------------------------------------------------------------- 1 | module foo { 2 | module bar; 3 | } 4 | 5 | module bar { 6 | module foobar; 7 | } 8 | 9 | module foobar { 10 | module foo; 11 | } -------------------------------------------------------------------------------- /data/error_policies/module_invalid.cas: -------------------------------------------------------------------------------- 1 | resource all_files {} 2 | 3 | domain all_processes { 4 | } 5 | 6 | module mod { 7 | resource all_files; 8 | } 9 | 10 | module modmod { 11 | domain all_files; 12 | resource nonexistent; 13 | module mods; 14 | } 15 | -------------------------------------------------------------------------------- /data/error_policies/named_resource_trans.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | resource_transition(domain, bar, [file dir], this, "something/test.txt"); 3 | 4 | // Policies must include at least one av rule 5 | allow(domain, foo, file, [read]); 6 | } 7 | 8 | resource bar {} 9 | -------------------------------------------------------------------------------- /data/error_policies/networking_rules.cas: -------------------------------------------------------------------------------- 1 | resource my_port { 2 | portcon("badprotocol", 1234, this); 3 | } 4 | -------------------------------------------------------------------------------- /data/error_policies/no_arg_function.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn no_args() { 3 | allow(domain, this, file, read); 4 | } 5 | } 6 | 7 | domain bar { 8 | foo.no_args(); 9 | } 10 | -------------------------------------------------------------------------------- /data/error_policies/nonexistent_inheritance.cas: -------------------------------------------------------------------------------- 1 | domain foo inherits bar {} 2 | -------------------------------------------------------------------------------- /data/error_policies/parse_unexpected_eof.cas: -------------------------------------------------------------------------------- 1 | domain all_processes { 2 | -------------------------------------------------------------------------------- /data/error_policies/parse_unknown_token.cas: -------------------------------------------------------------------------------- 1 | resource ☺ all_files {} 2 | -------------------------------------------------------------------------------- /data/error_policies/parse_unrecognized_token.cas: -------------------------------------------------------------------------------- 1 | resource all_files {}. 2 | -------------------------------------------------------------------------------- /data/error_policies/resource_trans.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | resource_transition(domain, bar, [quack], this); 3 | 4 | resource_transition(zap, bar, [file], aaa); 5 | resource_transition(bbb, bar, [file], foo); 6 | resource_transition(zap, ccc, [file], foo); 7 | 8 | // Policies must include at least one av rule 9 | allow(domain, foo, file, [read]); 10 | } 11 | 12 | resource bar {} 13 | 14 | domain zap { 15 | resource_transition(this, bar, [file dir], foo); 16 | } 17 | -------------------------------------------------------------------------------- /data/error_policies/self_function.cas: -------------------------------------------------------------------------------- 1 | resource my_file { 2 | my_domain.read(self); 3 | } 4 | 5 | domain my_domain { 6 | fn read(resource r) { 7 | allow(this, r, file, [ read open getattr ]); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /data/error_policies/self_inherit.cas: -------------------------------------------------------------------------------- 1 | resource foo inherits self {} 2 | 3 | domain qux inherits self { 4 | allow(this, self, file, read); 5 | } 6 | -------------------------------------------------------------------------------- /data/error_policies/self_subject.cas: -------------------------------------------------------------------------------- 1 | resource foo {} 2 | 3 | domain qux{ 4 | allow(self, foo, file, read); 5 | } 6 | -------------------------------------------------------------------------------- /data/error_policies/trait_no_impl.cas: -------------------------------------------------------------------------------- 1 | trait resource my_trait { 2 | fn write(domain source) { 3 | allow(source, this, file, write); 4 | } 5 | } 6 | 7 | @noderive 8 | resource foo inherits my_trait {} 9 | -------------------------------------------------------------------------------- /data/error_policies/unsupplied_arg.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn read(domain source, resource target) { 3 | allow(source, target, file, read); 4 | } 5 | } 6 | 7 | domain bar { 8 | foo.read(target=foo); 9 | } 10 | -------------------------------------------------------------------------------- /data/error_policies/virtual_function_association.cas: -------------------------------------------------------------------------------- 1 | virtual resource tmp { 2 | // All children must implement read 3 | virtual fn read(domain source) {} 4 | } 5 | 6 | @associate([tmp]) 7 | virtual domain foo { 8 | extend tmp { 9 | fn read(domain source) { 10 | allow(source, this, file, read); 11 | } 12 | } 13 | } 14 | 15 | domain bar inherits foo { 16 | 17 | @noderive 18 | extend tmp {} 19 | } 20 | -------------------------------------------------------------------------------- /data/error_policies/virtual_function_illegal_call.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo_parent { 2 | virtual fn foo_func(domain source) {} 3 | } 4 | 5 | domain bar { 6 | foo_parent.foo(this); // Illegal call 7 | } 8 | -------------------------------------------------------------------------------- /data/error_policies/virtual_function_non_define.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo_parent { 2 | virtual fn foo_func(domain source) {} 3 | } 4 | 5 | @noderive 6 | resource foo inherits foo_parent {} // Illegal non-inheritance 7 | -------------------------------------------------------------------------------- /data/policies/alias.cas: -------------------------------------------------------------------------------- 1 | @alias(foo) 2 | @alias(bar) 3 | resource baz { 4 | @alias(list) 5 | fn read(domain source) { 6 | allow(source, this, dir, read); 7 | } 8 | } 9 | 10 | domain quz { 11 | allow(this, foo, file, read); 12 | allow(this, bar, file, write); 13 | allow(this, baz, file, open); 14 | foo.list(this); 15 | } 16 | -------------------------------------------------------------------------------- /data/policies/arg_call.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo { 2 | fn read(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | } 6 | 7 | resource bar1 inherits foo { 8 | fn read(domain source) { 9 | allow(source, this, dir, read); 10 | } 11 | } 12 | resource bar2 inherits foo { 13 | fn read(domain source) { 14 | allow(source, this, lnk_file, read); 15 | } 16 | } 17 | resource bar3 inherits foo { 18 | fn read(domain source) { 19 | allow(source, this, chr_file, read); 20 | } 21 | } 22 | 23 | resource baz { 24 | fn call_source_read(foo source, domain arg) { 25 | source.read(arg); 26 | } 27 | } 28 | 29 | domain dom1 { 30 | baz.call_source_read(bar1, this); 31 | } 32 | 33 | domain dom2 { 34 | baz.call_source_read(bar2, this); 35 | } 36 | 37 | domain dom3 { 38 | fn call_in_function(domain something) { 39 | baz.call_source_read(bar3, this); 40 | } 41 | } 42 | 43 | @associate([foo]) 44 | domain dom4 { 45 | baz.call_source_read(this.foo, this); 46 | } 47 | -------------------------------------------------------------------------------- /data/policies/arguments.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn some_func(path a, string b, foo c, domain d) { 3 | allow(d, c, file, [read]); 4 | } 5 | } 6 | 7 | domain bar { 8 | foo.some_func("/test", "test", foo, bar); 9 | } 10 | -------------------------------------------------------------------------------- /data/policies/associate.cas: -------------------------------------------------------------------------------- 1 | virtual resource tmp { 2 | @associated_call 3 | fn associated_call_from_tmp(domain source) { 4 | allow(source, tmp, file, [read]); 5 | } 6 | 7 | fn not_an_associated_call(domain source) { 8 | allow(source, tmp, file, [write]); 9 | } 10 | } 11 | 12 | virtual resource user_tmp { 13 | @associated_call 14 | fn associated_call_from_user_tmp(userdomain source) { 15 | allow(source, user_tmp, file, [read]); 16 | } 17 | } 18 | 19 | virtual resource var { 20 | @associated_call 21 | fn associated_call_from_var(domain source) { 22 | allow(source, var, file, [read]); 23 | } 24 | } 25 | 26 | virtual resource bin { 27 | // no @associated_call 28 | fn not_an_associated_call_from_bin(domain source) { 29 | allow(source, bin, file, [read]); 30 | } 31 | } 32 | 33 | @associate([tmp var]) 34 | virtual domain foo { 35 | // Creates new resources foo.tmp and foo.var, and implicitly calls 36 | // foo.tmp.associated_call_from_tmp(foo) and foo.var.associated_call_from_var(foo) 37 | // 38 | // foo.tmp inherits tmp 39 | // foo.var inherits var 40 | 41 | tmp.associated_call_from_tmp(this); 42 | tmp.not_an_associated_call(this); 43 | this.tmp.not_an_associated_call(this); 44 | } 45 | 46 | @associate([bin]) 47 | virtual domain bar inherits foo { 48 | // Creates new resources bar.tmp, bar.var and bar.bin, and implicitly calls 49 | // bar.tmp.associated_call_from_tmp(bar), bar.var.associated_call_from_var(bar) and 50 | // bar.bin.associated_call_from_var(bar) 51 | // 52 | // bar.bin inherits bin 53 | // bar.tmp inherits foo.tmp 54 | // bar.var inherits foo.var 55 | } 56 | 57 | domain baz inherits bar { 58 | // Creates new resources baz.tmp, baz.var and baz.bin, and implicitly calls 59 | // baz.tmp.associated_call_from_tmp(baz), baz.var.associated_call_from_var(baz) and 60 | // baz.bin.associated_call_from_var(baz) 61 | // 62 | // baz.bin inherits bar.bin 63 | // baz.tmp inherits bar.tmp 64 | // baz.var inherits bar.var 65 | } 66 | 67 | domain qux { 68 | // Calls synthetic functions. 69 | foo.tmp.associated_call_from_tmp(this); 70 | bar.tmp.associated_call_from_tmp(this); 71 | baz.tmp.associated_call_from_tmp(this); 72 | 73 | // Explicit reference 74 | allow(this, bar.bin, file, write); 75 | } 76 | 77 | virtual domain nest_parent { 78 | virtual resource nest_resource { 79 | @associated_call 80 | fn call(domain source) { 81 | allow(source, this, file, read); 82 | } 83 | } 84 | 85 | allow(this, nest_resource, file, write); 86 | } 87 | 88 | domain nest_child inherits nest_parent { 89 | extend nest_resource { 90 | allow(nest_child, this, file, ioctl); 91 | file_context("/some/path/to/file", [file], system_u:object_r:nest_child.nest_resource:s0); 92 | file_context("/some/path/to/file2", [file], this); 93 | } 94 | 95 | foo.tmp.not_an_associated_call(); 96 | } 97 | 98 | extend nest_child { 99 | extend nest_resource { 100 | allow(nest_child, this, dir, remove_name); 101 | } 102 | } 103 | 104 | virtual domain userdomain {} 105 | 106 | @associate([user_tmp]) 107 | domain foo_userdomain inherits userdomain { 108 | // Creates new resources foo_userdomain.user_tmp, and implicitly calls 109 | // foo_userdomain.user_tmp.associated_call_from_user_tmp(foo_userdomain) 110 | // 111 | // foo_userdomain.user_tmp inherits tmp 112 | } 113 | 114 | virtual domain diamond1 inherits foo {} 115 | virtual domain diamond2 inherits foo {} 116 | 117 | // Gets two copies of associated resources via multiple inheritance, but they should collapse to one 118 | domain diamond3 inherits diamond1, diamond2 {} 119 | 120 | 121 | virtual resource mix {} 122 | 123 | virtual domain dom_with_mix { 124 | resource mix {} 125 | } 126 | 127 | @associate([mix]) 128 | virtual domain dom_with_mix_2 {} 129 | 130 | domain dom_with_mix_3 inherits dom_with_mix, dom_with_mix_2 {} 131 | -------------------------------------------------------------------------------- /data/policies/attribute.cas: -------------------------------------------------------------------------------- 1 | virtual domain user_type {} 2 | 3 | domain staff inherits user_type { 4 | // Policies must contain at least one AV rule 5 | allow(staff, resource, file, [read]); 6 | } 7 | -------------------------------------------------------------------------------- /data/policies/auditallow.cas: -------------------------------------------------------------------------------- 1 | resource foo {} 2 | 3 | domain my_domain { 4 | auditallow(this, foo, file, [read]); 5 | } 6 | -------------------------------------------------------------------------------- /data/policies/automatic_derive.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo { 2 | fn read(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | 6 | fn write(domain source) { 7 | allow(source, this, file, write); 8 | } 9 | } 10 | 11 | resource child inherits foo, bar { 12 | @alias(write) 13 | fn setattr(domain source) { 14 | allow(source, this, lnk_file, write); 15 | } 16 | } 17 | 18 | domain dom { 19 | child.read(); 20 | } 21 | 22 | virtual resource bar { 23 | @alias(read) 24 | fn list(domain source) { 25 | allow(source, this, dir, read); 26 | } 27 | 28 | @alias(write) 29 | fn setattr(domain source) { 30 | allow(source, this, file, write); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /data/policies/call_casting.cas: -------------------------------------------------------------------------------- 1 | virtual resource bar { 2 | fn read(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | fn foobar(domain source) { 6 | allow(source, this, dir, search); 7 | } 8 | } 9 | 10 | resource foo inherits bar { 11 | fn read(domain source) { 12 | allow(source, this, dir, read); 13 | } 14 | fn foobar(domain source) { 15 | allow(source, this, dir, search); 16 | } 17 | } 18 | 19 | domain dom { 20 | foo.read(this); 21 | foo.foobar(this); 22 | } 23 | 24 | virtual resource abc { 25 | fn read(domain source) { 26 | allow(source, this, file, read); 27 | } 28 | } 29 | 30 | resource xyz inherits abc { 31 | fn read(domain source) { 32 | abc.read(source); 33 | } 34 | } 35 | 36 | domain asd { 37 | asd.read(this); 38 | foo.read(this); 39 | } 40 | 41 | domain jkl inherits boo { 42 | jkl.read_boo_tmp(this); 43 | } 44 | 45 | virtual resource tmp { 46 | @associated_call 47 | fn associated_call_from_tmp(domain source) { 48 | allow(source, tmp, file, [read]); 49 | } 50 | } 51 | 52 | @associate([tmp]) 53 | virtual domain boo { 54 | // Creates new resources boo.tmp and implicitly calls 55 | // boo.tmp.associated_call_from_tmp(boo) 56 | // 57 | // boo.tmp inherits tmp 58 | 59 | fn read_boo_tmp(domain source) { 60 | allow(source, boo.tmp, file, [read]); 61 | } 62 | } -------------------------------------------------------------------------------- /data/policies/casting.cas: -------------------------------------------------------------------------------- 1 | domain foo { 2 | allow(foo, foo, capability, [dac_override]); 3 | allow(this, this, capability, [mac_override]); 4 | 5 | fn signal(domain source) { 6 | allow(source, this, process, signal); 7 | } 8 | } 9 | 10 | domain bar { 11 | foo.signal(this); 12 | 13 | this.signal(this); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /data/policies/collection.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn read_foo(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | } 6 | 7 | resource bar { 8 | fn read_bar(domain source) { 9 | allow(source, this, file, read); 10 | } 11 | } 12 | 13 | collection foobar_reader { 14 | @alias(collection_alias) 15 | fn read_foo_and_bar(domain source) { 16 | foo.read_foo(source); 17 | bar.read_bar(source); 18 | } 19 | } 20 | 21 | domain baz { 22 | foobar_reader.read_foo_and_bar(this); 23 | foobar_reader.collection_alias(this); 24 | } 25 | -------------------------------------------------------------------------------- /data/policies/conditional.cas: -------------------------------------------------------------------------------- 1 | resource foo {} 2 | 3 | let my_tunable = false; 4 | let my_tunable_2 = true; 5 | 6 | domain bar { 7 | if (true) { 8 | allow(this, foo, file, write); 9 | } else { 10 | allow(this, fool, file, read); 11 | } 12 | 13 | if (false) { 14 | allow(this, foo, file, entrypoint); 15 | } 16 | 17 | if (my_tunable) { 18 | allow(this, foo, file, getattr); 19 | } 20 | 21 | if (my_tunable && my_tunable_2) { 22 | allow(this, foo, file, setattr); 23 | } 24 | 25 | if (my_tunable || my_tunable_2) { 26 | allow(this, foo, file, open); 27 | } 28 | 29 | if (!my_tunable) { 30 | allow(this, foo, file, execute); 31 | } 32 | 33 | if (!(my_tunable && my_tunable_2)) { 34 | allow(this, foo, file, lock); 35 | } 36 | 37 | if (my_tunable && !my_tunable_2) { 38 | allow(this, foo, file, ioctl); 39 | } 40 | 41 | // Without full conditional support, the above lines are just ignored 42 | // TODO: implement full conditional support and remove this 43 | allow(this, foo, file, getattr); 44 | } 45 | -------------------------------------------------------------------------------- /data/policies/default.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn read(domain source=bar) { 3 | allow(source, this, file, read); 4 | } 5 | 6 | fn write_something(domain source, resource target=foo) { 7 | allow(source, target, file, write); 8 | } 9 | } 10 | 11 | domain bar {} 12 | 13 | domain baz { 14 | foo.read(); 15 | foo.read(this); 16 | foo.write_something(this); 17 | foo.write_something(this, target=resource); 18 | } 19 | -------------------------------------------------------------------------------- /data/policies/derive.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo { 2 | fn read(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | 6 | @associated_call 7 | fn some_associated_call(domain source) { 8 | allow(source, this, file, link); 9 | } 10 | } 11 | 12 | virtual resource bar { 13 | fn read(domain source) { 14 | allow(source, this, dir, read); 15 | } 16 | 17 | fn write(domain source) { 18 | allow(source, this, dir, write); 19 | } 20 | 21 | @associated_call 22 | fn some_associated_call(domain source) { 23 | allow(source, this, dir, add_name); 24 | } 25 | } 26 | 27 | resource custom_define inherits foo, bar { 28 | fn read(domain source) { 29 | allow(source, this, lnk_file, read); 30 | } 31 | } 32 | 33 | @derive([read], parents=*) 34 | resource union_all_parents inherits foo, bar {} 35 | 36 | @derive([read], parents=foo) 37 | resource derive_from_foo inherits foo, bar {} 38 | 39 | // This derives bar.write() while unioning foo.read() and bar.read() 40 | @derive(*, parents=foo) 41 | resource derive_from_foo2 inherits foo, bar {} 42 | 43 | @derive(*, *) 44 | resource derive_all inherits foo, bar {} 45 | 46 | @derive(*) 47 | resource defaults inherits foo, bar {} 48 | 49 | @derive(*, parents=[foo bar]) 50 | resource enumerate_parents inherits foo, bar {} 51 | 52 | domain some_domain { 53 | union_all_parents.read(this); 54 | derive_from_foo.read(this); 55 | custom_define.read(this); 56 | name_diff_child.diff_name(this); 57 | an_alias.read(); 58 | 59 | some_child.domtrans(this, derive_from_foo); 60 | 61 | fn call_derive_in_func(domain arg) { 62 | union_all_parents.read(arg); 63 | } 64 | } 65 | 66 | @derive([some_associated_call], parents=*) 67 | virtual resource to_associate inherits foo, bar {} 68 | 69 | @associate([to_associate]) 70 | domain associates {} 71 | 72 | virtual resource name_diff1 { 73 | fn diff_name(domain a) { 74 | allow(a, this, file, relabelfrom); 75 | } 76 | } 77 | 78 | virtual resource name_diff2 { 79 | fn diff_name(domain b) { 80 | allow(b, this, file, relabelto); 81 | } 82 | } 83 | 84 | @derive([diff_name], parents=*) 85 | resource name_diff_child inherits name_diff1, name_diff2 {} 86 | 87 | virtual domain some_domain_parent { 88 | fn domtrans(domain source, resource exec) { 89 | domain_transition(source, exec, this); 90 | } 91 | } 92 | 93 | @derive([domtrans], parents=[some_domain_parent]) 94 | domain some_child inherits some_domain_parent {} 95 | 96 | @derive(*, *) 97 | resource overwrite_one inherits bar { 98 | // define our own read 99 | fn read(domain source) { 100 | allow(source, this, lnk_file, read); 101 | } 102 | } 103 | 104 | @derive(*, *) 105 | resource overwrite_two inherits bar { 106 | // define our own read 107 | fn read(domain source) { 108 | allow(source, this, lnk_file, read); 109 | } 110 | 111 | // define our own write 112 | fn write(domain source) { 113 | allow(source, this, lnk_file, write); 114 | } 115 | } 116 | 117 | @alias(an_alias) 118 | resource aliased_child inherits bar {} 119 | 120 | // Derive arg call test 121 | 122 | virtual resource a { 123 | fn read(domain source) { 124 | allow(source, this, file, read); 125 | } 126 | fn write(domain source) { 127 | allow(source, this, file, write); 128 | } 129 | } 130 | 131 | resource b1 inherits a { 132 | fn read(domain source) { 133 | allow(source, this, lnk_file, read); 134 | } 135 | fn write(domain source) { 136 | allow(source, this, lnk_file, write); 137 | } 138 | } 139 | 140 | resource b2 inherits a {} 141 | 142 | // c and d have similar functions with different arg names 143 | virtual resource c { 144 | fn call_arg(a to_call_read, domain source) { 145 | to_call_read.read(source); 146 | } 147 | } 148 | 149 | virtual resource d { 150 | fn call_arg(a to_call_write, domain source) { 151 | to_call_write.write(source); 152 | } 153 | } 154 | 155 | // Should derive call_arg with read and write and a rewritten argument name 156 | resource e inherits c,d {} 157 | 158 | domain f { 159 | e.call_arg(b1, this); 160 | e.call_arg(b2, this); 161 | } 162 | 163 | // Derive with this 164 | virtual resource derive_this_2 inherits foo { 165 | fn my_func(domain source) { 166 | this.read(source); 167 | } 168 | 169 | fn use_this(domain source) { 170 | this.my_func(source); 171 | } 172 | } 173 | 174 | virtual resource derive_this_1 { 175 | fn use_this(domain source) { 176 | // do something else 177 | allow(source, this, file, read); 178 | } 179 | } 180 | 181 | resource derive_this_3 inherits derive_this_2, derive_this_1 {} 182 | 183 | domain call_derive_this { 184 | // Please never write real policy that uses the word "this" this confusingly 185 | derive_this_3.use_this(this); 186 | } 187 | 188 | virtual resource alias_parent1 { 189 | @alias(alias1) 190 | fn has_alias(domain source) { 191 | allow(source, this, file, read); 192 | } 193 | } 194 | 195 | virtual resource alias_parent2 inherits alias_parent1 { 196 | fn alias1(domain source) { 197 | allow(source, this, file, write); 198 | } 199 | } 200 | 201 | @alias(alias2) 202 | resource my_res inherits alias_parent2 {} 203 | 204 | virtual resource g { 205 | fn func(domain source) { 206 | allow(source, this, file, read); 207 | } 208 | } 209 | 210 | virtual resource h { 211 | @alias(func) 212 | fn func2(domain source) { 213 | allow(source, this, file, write); 214 | } 215 | } 216 | 217 | @alias(j) 218 | virtual resource i inherits g, h {} 219 | 220 | resource k inherits j {} 221 | -------------------------------------------------------------------------------- /data/policies/direct_association_reference.cas: -------------------------------------------------------------------------------- 1 | virtual resource associated {} 2 | 3 | @associate([associated]) 4 | domain foo { 5 | allow(this, foo.associated, file, read); 6 | allow(this, this.associated, file, read); 7 | } 8 | -------------------------------------------------------------------------------- /data/policies/domtrans.cas: -------------------------------------------------------------------------------- 1 | resource foo_exec {} 2 | 3 | domain foo {} 4 | 5 | domain bar { 6 | domain_transition(bar, foo_exec, foo); 7 | allow(bar, foo_exec, file, [ read open execute]); 8 | allow(foo, foo_exec, file, entrypoint); 9 | // TODO: there is a bug in allow rules that doesn't allow domains in targets. Add transition rule once that's fixed 10 | } 11 | -------------------------------------------------------------------------------- /data/policies/dontaudit.cas: -------------------------------------------------------------------------------- 1 | resource foo {} 2 | 3 | domain my_domain { 4 | dontaudit(this, foo, file, [read]); 5 | } 6 | -------------------------------------------------------------------------------- /data/policies/drop.cas: -------------------------------------------------------------------------------- 1 | resource foo {} 2 | 3 | domain bar { 4 | allow(this, foo, file, [read write append]); 5 | 6 | drop allow(this, foo, file, append); 7 | } 8 | -------------------------------------------------------------------------------- /data/policies/duplicate_inherit.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo {} 2 | 3 | virtual resource bar inherits foo {} 4 | 5 | virtual resource baz inherits foo {} 6 | 7 | resource qux inherits bar, baz {} 8 | 9 | domain requires_allow { 10 | allow(this, qux, file, read); 11 | } -------------------------------------------------------------------------------- /data/policies/extend.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | allow(bar, this, file, getattr); 3 | } 4 | 5 | extend foo { 6 | fn my_func(domain source) { 7 | allow(source, this, file, read); 8 | } 9 | 10 | allow(bar, this, file, write); 11 | } 12 | 13 | domain bar { 14 | foo.my_func(this); 15 | } 16 | 17 | extend resource { 18 | fn read(domain source) { 19 | allow(source, this, file, read); 20 | } 21 | } 22 | 23 | // Add an alias 24 | @alias(some_alias) 25 | extend foo {} 26 | 27 | // Add an associated resource in an extend block 28 | extend bar { 29 | resource baz {} 30 | } 31 | -------------------------------------------------------------------------------- /data/policies/extend_inherit.cas: -------------------------------------------------------------------------------- 1 | virtual resource bar {} 2 | 3 | domain dom { 4 | resource foo {} 5 | allow(this, this.foo, file, read); 6 | } 7 | 8 | extend dom { 9 | extend foo inherits bar {} 10 | } 11 | 12 | virtual domain dom_parent {} 13 | 14 | extend dom inherits dom_parent {} 15 | -------------------------------------------------------------------------------- /data/policies/filecon.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | file_context("/bin", [file dir], foo); 3 | file_context("/etc", [any], this); 4 | file_context("/dev/sda1", [blk_file], this); 5 | file_context("/dev/tty.*", [chr_file], this); 6 | file_context("/etc/somesymlink", [lnk_file], this); 7 | file_context("/var/somepipe", [fifo_file], this); 8 | file_context("/var/somesocket", [sock_file], this); 9 | file_context("/bin/some_bin", [file], system_u:object_r:foo:s0); 10 | file_context("/bin/some_bin2", [file], system_u:object_r:foo:s0-s0); 11 | file_context("/bin/some_bin3", [file], system_u:object_r:foo:s0-s0:c0.c255); 12 | file_context("/bin/some_bin4", [file], system_u:object_r:this:s0); 13 | file_context("HOME_ROOT", dir, this); 14 | // Policies must include at least one av rule 15 | allow(domain, foo, file, [read]); 16 | } 17 | -------------------------------------------------------------------------------- /data/policies/fs_context.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fs_context("ext3", xattr, foo); 3 | fs_context("sockfs", task, this); 4 | fs_context("tmpfs", trans, this); 5 | fs_context("tmpfs", trans, this); 6 | fs_context("hugetblfs", xattr, system_u:object_r:this); 7 | 8 | fs_context("proc", genfscon, this, "/"); 9 | fs_context("proc", genfscon, this, "/"); 10 | fs_context("cgroup", genfscon, this); 11 | fs_context("proc", genfscon, system_u:object_r:foo, "/foo"); 12 | 13 | // TODO re-add when secilc check is in place 14 | // fs_context("sysfs", genfscon, this, "/zap", [dir]); 15 | 16 | // Policies must include at least one av rule 17 | allow(domain, foo, file, [read]); 18 | } 19 | 20 | // TODO re-add when secilc check is in place 21 | // resource bar { 22 | // fs_context("sysfs", genfscon, this, "/zap/baa", [file]); 23 | //} 24 | -------------------------------------------------------------------------------- /data/policies/full_system/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a sample full system cascade policy intended for 2 | demonstrating and testing Cascade. 3 | 4 | The target system for this policy is an unmodified Fedora Workstation 36. 5 | 6 | The intent is to create a policy that boots in enforcing mode and allows simple 7 | tests and demonstrations to be run on that system. This is not intended as a 8 | basis for a production policy and should not be used on any system you care 9 | about. 10 | 11 | To build the policy in this directory, run: 12 | 13 | ``` 14 | casc --package -m cascade data/policies/full_system/*.cas 15 | ``` 16 | 17 | That will create a tarball of a policy named cascade that can be staged at 18 | /etc/selinux. 19 | 20 | To switch the system to use the Cascade policy, you can modify 21 | /etc/selinux/config to reference the "cascade" policy name. 22 | 23 | The files in the contexts folders are SELinux configuration files that need to 24 | be staged at the following locations on the target system: 25 | 26 | /etc/selinux/cascade/contexts/default_contexts 27 | /etc/selinux/cascade/contexts/virtual_domain_context 28 | /etc/selinux/cascade/contexts/virtual_image_context 29 | /etc/selinux/cascade/contexts/x_contexts 30 | 31 | Eventually, Cascade plans to add the functionality to generate these files, but 32 | in the meantime, they need to be manually staged. Cascade does currently stage 33 | dbus_contexts and seusers automatically when the --package flag is used. 34 | 35 | If any other context files are needed for your use case, you will need to add 36 | them manually. 37 | 38 | In order to use the Cascade system, you can relabel by rebooting into 39 | permissive mode with your Cascade policy, running: 40 | 41 | ``` 42 | restorecon -RF / 43 | ``` 44 | 45 | And then rebooting again in enforcing mode. 46 | -------------------------------------------------------------------------------- /data/policies/full_system/all_files.cas: -------------------------------------------------------------------------------- 1 | @derive([unconfined_access], parents=*) 2 | resource all_files inherits file_like_objects { 3 | file_context("/.*", [any], this); 4 | 5 | allow(this, this, filesystem, [associate]); 6 | 7 | // Filesystem labels for filesystems on a default Fedora install 8 | // If you are using this policy on a different system, you may need 9 | // to add labels for any other used filesystems here 10 | fs_context("proc", genfscon, this, "/"); 11 | fs_context("selinuxfs", genfscon, this, "/"); 12 | fs_context("securityfs", genfscon, this, "/"); 13 | fs_context("sysfs", genfscon, this, "/"); 14 | fs_context("bpf", genfscon, this, "/"); 15 | fs_context("cgroup2", genfscon, this, "/"); 16 | fs_context("pstore", genfscon, this, "/"); 17 | fs_context("debugfs", genfscon, this, "/"); 18 | fs_context("tracefs", genfscon, this, "/"); 19 | fs_context("autofs", genfscon, this, "/"); 20 | fs_context("configfs", genfscon, this, "/"); 21 | fs_context("rpc_pipefs", genfscon, this, "/"); 22 | fs_context("fuse", genfscon, this, "/"); 23 | fs_context("fusectl", genfscon, this, "/"); 24 | fs_context("vboxsf", genfscon, this, "/"); 25 | fs_context("nsfs", genfscon, this, "/"); 26 | fs_context("btrfs", xattr, this); 27 | fs_context("ext4", xattr, this); 28 | fs_context("xfs", xattr, this); 29 | fs_context("devtmpfs", trans, this); 30 | fs_context("tmpfs", trans, this); 31 | fs_context("hugetlbfs", trans, this); 32 | fs_context("devpts", trans, this); 33 | fs_context("mqueue", trans, this); 34 | fs_context("pipefs", task, this); 35 | fs_context("sockfs", task, this); 36 | } 37 | 38 | extend unlabeled_sid inherits file_like_objects {} 39 | 40 | resource ssh_keys inherits file_like_objects { 41 | file_context("/home/dburgener/.ssh/id_rsa", [file], this); 42 | allow(this, all_files, filesystem, [associate]); 43 | } 44 | -------------------------------------------------------------------------------- /data/policies/full_system/base_resource.cas: -------------------------------------------------------------------------------- 1 | let common_file_perms = [ ioctl read write create getattr setattr lock relabelfrom relabelto append map unlink link rename execute quotaon mounton audit_access open execmod watch watch_mount watch_sb watch_with_perm watch_reads ]; 2 | let all_dir_perms = [ common_file_perms add_name remove_name reparent search rmdir ]; 3 | let all_file_perms = [ common_file_perms execute_no_trans entrypoint ]; 4 | 5 | virtual resource file_like_objects { 6 | fn unconfined_access(domain source) { 7 | allow(source, this, file, all_file_perms); 8 | allow(source, this, dir, all_dir_perms); 9 | allow(source, this, lnk_file, common_file_perms); 10 | allow(source, this, blk_file, common_file_perms); 11 | allow(source, this, fifo_file, common_file_perms); 12 | allow(source, this, chr_file, common_file_perms); 13 | allow(source, this, sock_file, common_file_perms); 14 | allow(source, this, service, [start stop status reload enable disable]); 15 | allow(source, this, filesystem, *); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /data/policies/full_system/contexts/default_contexts: -------------------------------------------------------------------------------- 1 | system_r:general system_r:general 2 | -------------------------------------------------------------------------------- /data/policies/full_system/contexts/virtual_domain_context: -------------------------------------------------------------------------------- 1 | system_u:system_r:general 2 | -------------------------------------------------------------------------------- /data/policies/full_system/contexts/virtual_image_context: -------------------------------------------------------------------------------- 1 | system_u:system_r:all_files 2 | -------------------------------------------------------------------------------- /data/policies/full_system/contexts/x_contexts: -------------------------------------------------------------------------------- 1 | client * system_u:object_r:all_files 2 | extension * system_u:object_r:all_files 3 | selection * system_u:object_r:all_files 4 | property * system_u:object_r:all_files 5 | event * system_u:object_r:all_files 6 | -------------------------------------------------------------------------------- /data/policies/full_system/general.cas: -------------------------------------------------------------------------------- 1 | @derive([domtrans], parents=*) 2 | domain general inherits unconfined {} 3 | 4 | @derive([unconfined_access], parents=*) 5 | resource init_exec inherits file_like_objects { 6 | file_context("/usr/lib/systemd/systemd", [file], this); 7 | } 8 | -------------------------------------------------------------------------------- /data/policies/full_system/kernel.cas: -------------------------------------------------------------------------------- 1 | // kernel_sid is currently the built in initial sid for the kernel 2 | // We extend it here to add needed permissions and functions. In the 3 | // longer term, we will make initial sids configurable rather than built in 4 | // (although a built-in option may be retained for simplicity) 5 | extend kernel_sid { 6 | all_files.unconfined_access(); 7 | general.domtrans(); 8 | 9 | allow(this, domain, file, *); 10 | allow(this, domain, dir, *); 11 | allow(this, domain, lnk_file, *); 12 | 13 | allow(this, this, capability, [ dac_read_search mknod net_admin sys_tty_config sys_admin sys_module sys_ptrace syslog ]); 14 | allow(this, this, system, [ module_load syslog_console ]); 15 | allow(this, this, process, [fork setcurrent]); 16 | allow(this, this, key, [ search ]); 17 | allow(this, security_sid, security, compute_create); 18 | 19 | allow(this, unlabeled_sid, association, *); 20 | 21 | this.kernel_ipc(); 22 | 23 | fn kernel_ipc(domain source) { 24 | allow(source, this, unix_dgram_socket, *); 25 | allow(source, this, unix_stream_socket, *); 26 | allow(this, source, unix_stream_socket, connectto); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /data/policies/full_system/machine_config.cas: -------------------------------------------------------------------------------- 1 | machine cascade { 2 | module unconfined; 3 | module kernel; 4 | module resources; 5 | let machine_type = standard; 6 | let handle_unknown_perms = allow; 7 | } 8 | 9 | module unconfined { 10 | domain unconfined; 11 | domain general; 12 | resource init_exec; 13 | } 14 | 15 | module kernel { 16 | domain kernel_sid; 17 | } 18 | 19 | module resources { 20 | resource file_like_objects; 21 | resource all_files; 22 | resource ssh_keys; 23 | } 24 | -------------------------------------------------------------------------------- /data/policies/full_system/unconfined.cas: -------------------------------------------------------------------------------- 1 | virtual domain unconfined { 2 | all_files.unconfined_access(); 3 | init_exec.unconfined_access(); 4 | 5 | allow(this, unlabeled_sid, dir, [ read getattr search mounton ]); 6 | allow(this, ssh_keys, file, [ getattr ]); 7 | 8 | allow(this, domain, file, *); 9 | allow(this, domain, dir, *); 10 | allow(this, domain, lnk_file, *); 11 | allow(this, domain, fifo_file, *); 12 | 13 | // TODO: support self keyword 14 | //allow(this, self, dbus, [acquire_svc send_msg]); 15 | allow(this, this, dbus, [acquire_svc send_msg]); 16 | allow(this, this, dbus, *); 17 | //allow(this, this, security, *); 18 | allow(this, security_sid, security, *); 19 | allow(this, domain, process, *); 20 | allow(this, domain, system, *); 21 | allow(this, this, capability, *); 22 | allow(this, this, cap_userns, *); 23 | allow(this, this, socket, *); 24 | allow(this, this, tcp_socket, *); 25 | allow(this, this, udp_socket, *); 26 | allow(this, this, node_socket, *); 27 | allow(this, this, rawip_socket, *); 28 | allow(this, this, netlink_socket, *); 29 | allow(this, this, netlink_route_socket, *); 30 | allow(this, this, netlink_generic_socket, *); 31 | allow(this, this, netlink_netfilter_socket, *); 32 | allow(this, this, netlink_selinux_socket, *); 33 | allow(this, domain, netlink_audit_socket, *); 34 | allow(this, this, netlink_kobject_uevent_socket, *); 35 | allow(this, this, packet_socket, *); 36 | allow(this, this, key_socket, *); 37 | allow(this, this, unix_stream_socket, *); 38 | allow(this, this, unix_dgram_socket, *); 39 | allow(this, this, sem, *); 40 | allow(this, this, shm, *); 41 | allow(this, this, msg, *); 42 | allow(this, this, msgq, *); 43 | allow(this, this, bpf, *); 44 | allow(this, domain, key, *); 45 | 46 | // X 47 | allow(this, this, x_application_data, *); 48 | allow(this, all_files, x_client, *); 49 | allow(this, this, x_colormap, *); 50 | allow(this, this, x_cursor, *); 51 | allow(this, this, x_cursor, *); 52 | allow(this, this, x_drawable, *); 53 | allow(this, all_files, x_event, *); 54 | allow(this, all_files, x_extension, *); 55 | allow(this, this, x_font, *); 56 | allow(this, this, x_gc, *); 57 | allow(this, this, x_keyboard, *); 58 | allow(this, this, x_pointer, *); 59 | allow(this, all_files, x_property, *); 60 | allow(this, this, x_resource, *); 61 | allow(this, this, x_screen, *); 62 | allow(this, all_files, x_selection, *); 63 | allow(this, this, x_server, *); 64 | allow(this, all_files, x_synthetic_event, *); 65 | 66 | // Port labeling is not yet implemented 67 | allow(this, unlabeled_sid, udp_socket, *); 68 | allow(this, unlabeled_sid, tcp_socket, *); 69 | allow(this, unlabeled_sid, association, *); 70 | 71 | kernel_sid.kernel_ipc(this); 72 | 73 | fn domtrans(domain source) { 74 | domain_transition(source, init_exec, this); 75 | allow(source, init_exec, file, [ getattr read execute open ]); 76 | allow(source, this, process, [ dyntransition signal transition ]); 77 | allow(this, source, fd, [ use ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /data/policies/function.cas: -------------------------------------------------------------------------------- 1 | resource my_file { 2 | fn read(domain source) { 3 | allow(source, this, file, [ read open getattr ]); 4 | } 5 | 6 | fn call_read(domain source) { 7 | this.read(source); 8 | } 9 | } 10 | 11 | domain my_domain { 12 | 13 | resource res { 14 | my_domain.access(this); 15 | } 16 | 17 | fn access(resource target) { 18 | allow(this, target, file, read); 19 | } 20 | 21 | this.access(this.res); 22 | 23 | my_file.read(this); 24 | my_file.call_read(); 25 | } 26 | -------------------------------------------------------------------------------- /data/policies/hint.cas: -------------------------------------------------------------------------------- 1 | // The @hint annotation hasn't been defined yet. This should compile cleanly 2 | // and return a warning saying that. 3 | @hint() 4 | resource foo {} 5 | 6 | domain bar { 7 | allow(this, foo, file, read); 8 | } 9 | -------------------------------------------------------------------------------- /data/policies/implicit_this.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn read(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | } 6 | 7 | domain bar { 8 | //equivalent to foo.read(this) 9 | foo.read(); 10 | } 11 | -------------------------------------------------------------------------------- /data/policies/inherit_alias.cas: -------------------------------------------------------------------------------- 1 | @alias(an_alias) 2 | virtual resource some_res {} 3 | 4 | resource child inherits an_alias {} 5 | 6 | domain require_allow { 7 | allow(this, child, file, read); 8 | } 9 | -------------------------------------------------------------------------------- /data/policies/initial_context.cas: -------------------------------------------------------------------------------- 1 | resource kernel_con { 2 | initial_context("kernel", this); 3 | } 4 | 5 | resource unlabeled_con { 6 | initial_context("unlabeled", this); 7 | } 8 | 9 | resource security_con { 10 | initial_context("security", this); 11 | } 12 | 13 | resource other_con { 14 | initial_context("other", system_u:object_r:this); 15 | } 16 | 17 | domain some_dom { 18 | allow(this, unlabeled_con, file, read); 19 | } 20 | -------------------------------------------------------------------------------- /data/policies/let.cas: -------------------------------------------------------------------------------- 1 | let read_file_perms = [ read open getattr ]; 2 | 3 | @makelist 4 | let binding_with_annotation = read; 5 | 6 | // TODO: We could use some array concatenation syntactic sugar like '+' 7 | let rw_file_perms = [ read_file_perms write ]; 8 | 9 | resource bar { 10 | fn read(domain source) { 11 | allow(source, this, lnk_file, read); 12 | } 13 | } 14 | 15 | let baz = bar; 16 | 17 | domain foo { 18 | 19 | let internal_binding = entrypoint; 20 | let nested_binding = [ internal_binding setattr ]; 21 | 22 | allow(foo, bar, file, read_file_perms); 23 | auditallow(foo, bar, file, read_file_perms); 24 | allow(foo, bar, file, binding_with_annotation); 25 | allow(this, bar, file, internal_binding); 26 | allow(this, bar, file, nested_binding); 27 | allow(foo, baz, file, write); 28 | 29 | baz.read(); 30 | } 31 | 32 | let class_list = [ file dir ]; 33 | let cl2 = [ lnk_file class_list ]; 34 | -------------------------------------------------------------------------------- /data/policies/machine_building1.cas: -------------------------------------------------------------------------------- 1 | machine foo { 2 | module foo_mod1; 3 | module foo_mod2; 4 | let handle_unknown_perms = reject; 5 | } 6 | 7 | module foo_mod1 { 8 | module foo_mod1_mod; 9 | } 10 | 11 | module foo_mod1_mod { 12 | domain thud; 13 | resource babble; 14 | module foo_mod1_mod_mod; 15 | } 16 | 17 | module foo_mod1_mod_mod { 18 | domain xyzzy; 19 | } 20 | 21 | domain thud { 22 | allow(thud, babble, file, read_file_perms); 23 | } 24 | 25 | resource babble {} 26 | 27 | domain xyzzy {} 28 | 29 | resource unused { 30 | fn do_something(domain source) {} 31 | } 32 | -------------------------------------------------------------------------------- /data/policies/machine_building2.cas: -------------------------------------------------------------------------------- 1 | machine bar { 2 | module foo_mod2; 3 | let handle_unknown_perms = deny; 4 | } 5 | 6 | module foo_mod2 { 7 | resource qux; 8 | module foo_mod2_mod1; 9 | module foo_mod2_mod2; 10 | } 11 | 12 | module foo_mod2_mod1 { 13 | domain baz; 14 | } 15 | 16 | module foo_mod2_mod2 { 17 | domain quuz; 18 | } 19 | 20 | virtual resource quux { 21 | virtual fn read(domain source) {} 22 | } 23 | 24 | resource qux inherits quux { 25 | fn read(domain source) { 26 | allow(source, this, file, read_file_perms); 27 | } 28 | } 29 | 30 | domain baz {} 31 | domain quuz { 32 | qux.read(this); 33 | } 34 | -------------------------------------------------------------------------------- /data/policies/machine_building3.cas: -------------------------------------------------------------------------------- 1 | machine foobar { 2 | module foobar_mod; 3 | let handle_unknown_perms = allow; 4 | } 5 | 6 | module foobar_mod { 7 | resource unused; 8 | } 9 | 10 | let read_file_perms = read; 11 | 12 | extend thud { 13 | allow(thud, babble, file, write); 14 | } 15 | -------------------------------------------------------------------------------- /data/policies/machines.cas: -------------------------------------------------------------------------------- 1 | machine name { 2 | module mod; 3 | let machine_type = standard; 4 | let handle_unknown_perms = allow; 5 | } 6 | 7 | module mod { 8 | domain foo; 9 | resource bar; 10 | } 11 | 12 | domain foo { 13 | allow(this, bar, file, read); 14 | } 15 | 16 | resource bar {} 17 | -------------------------------------------------------------------------------- /data/policies/makelist.cas: -------------------------------------------------------------------------------- 1 | @makelist 2 | resource foo { 3 | fn foo_func([foo] types) { 4 | allow(foo_dom, foo, file, read); 5 | } 6 | } 7 | 8 | domain foo_dom { 9 | foo.foo_func(foo); 10 | } 11 | -------------------------------------------------------------------------------- /data/policies/module_alias.cas: -------------------------------------------------------------------------------- 1 | domain quz { 2 | allow(this, baz, file, read); 3 | } 4 | 5 | @alias(baz) 6 | resource thud {} 7 | 8 | @alias(foo) 9 | @alias(bar) 10 | module babble { 11 | resource baz; 12 | } 13 | 14 | module qux { 15 | domain quz; 16 | module bar; 17 | } 18 | -------------------------------------------------------------------------------- /data/policies/module_arguments.cas: -------------------------------------------------------------------------------- 1 | resource foobar { 2 | fn some_func(path a, string b, foobar c, domain d) { 3 | allow(d, c, file, [read]); 4 | } 5 | } 6 | 7 | domain xyzzy { 8 | foobar.some_func("/test", "test", foobar, xyzzy); 9 | } 10 | 11 | module quux { 12 | resource foobar; 13 | domain xyzzy; 14 | } -------------------------------------------------------------------------------- /data/policies/module_simple.cas: -------------------------------------------------------------------------------- 1 | resource all_files {} 2 | 3 | domain all_processes { 4 | allow(all_processes, all_files, file, [read write open getattr append]); 5 | } 6 | 7 | module mod { 8 | resource all_files; 9 | } 10 | 11 | module modmod { 12 | domain all_processes; 13 | module mod; 14 | } -------------------------------------------------------------------------------- /data/policies/multifile1.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo { 2 | fn read(domain source) { 3 | allow(source, this, file, read); 4 | } 5 | } 6 | 7 | virtual domain extend_across_files { 8 | resource res1 {} 9 | } 10 | -------------------------------------------------------------------------------- /data/policies/multifile2.cas: -------------------------------------------------------------------------------- 1 | resource bar inherits foo {} 2 | 3 | domain baz { 4 | foo.read(this); 5 | } 6 | 7 | extend extend_across_files { 8 | resource res2 {} 9 | } 10 | -------------------------------------------------------------------------------- /data/policies/named_args.cas: -------------------------------------------------------------------------------- 1 | domain some_domain { 2 | fn three_args(resource a, resource b, resource c) { 3 | allow(this, a, file, write); 4 | allow(this, b, file, read); 5 | allow(this, c, file, open); 6 | } 7 | 8 | this.three_args(a=foo, b=bar, c=baz); 9 | this.three_args(c=foo, b=baz, a=bar); 10 | } 11 | 12 | resource foo {} 13 | resource bar {} 14 | resource baz {} 15 | -------------------------------------------------------------------------------- /data/policies/named_resource_trans.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | resource_transition(domain, bar, [file dir], this, "test.txt"); 3 | 4 | fn foo_filetrans(domain source, resource new_type, [class] classes, string name) { 5 | resource_transition(source, new_type, classes, this, name); 6 | } 7 | 8 | // Policies must include at least one av rule 9 | allow(domain, foo, file, [read]); 10 | } 11 | 12 | resource bar {} 13 | 14 | domain some_dom { 15 | foo.foo_filetrans(this, bar, [lnk_file], "test.sh"); 16 | } 17 | -------------------------------------------------------------------------------- /data/policies/nested_alias.cas: -------------------------------------------------------------------------------- 1 | virtual resource tmp { 2 | // All children must implement read 3 | virtual fn read(domain source) {} 4 | } 5 | 6 | @associate([tmp]) 7 | virtual domain foo { 8 | extend tmp { 9 | fn read(domain source) { 10 | allow(source, this, file, read); 11 | } 12 | } 13 | } 14 | 15 | domain bar inherits foo { 16 | allow(this, bob, file, [read]); 17 | 18 | @alias(zap) 19 | extend tmp {} 20 | } 21 | 22 | domain abc { 23 | allow(this, bob, file, [read]); 24 | allow(this, zap, file, [read]); 25 | resource xyz {} 26 | 27 | @alias(bob) 28 | extend xyz {} 29 | } 30 | -------------------------------------------------------------------------------- /data/policies/nested_conflict.cas: -------------------------------------------------------------------------------- 1 | collection name { 2 | fn func(domain source) { 3 | allow(source, self, capability, sys_ptrace); 4 | } 5 | 6 | fn conflict_func(domain source) { 7 | allow(source, self, capability, sys_admin); 8 | } 9 | } 10 | 11 | virtual resource name_parent { 12 | fn conflict_func(domain source) { 13 | allow(source, this, file, read); 14 | } 15 | } 16 | 17 | domain foo { 18 | // Should only derive a function from name_parent, not collection name 19 | resource name inherits name_parent {} 20 | } 21 | 22 | domain bar { 23 | name.func(foo); 24 | } 25 | -------------------------------------------------------------------------------- /data/policies/networking_rules.cas: -------------------------------------------------------------------------------- 1 | let ssh_port = 22; 2 | 3 | resource my_port { 4 | portcon("tcp", 1234, this); 5 | portcon("UDP", 1235, this); 6 | portcon("tcp", 5000-5010, this); 7 | portcon("tcp", ssh_port, this); 8 | portcon("dccp", 1337, this); 9 | portcon("sctp", 43, this); 10 | portcon("tcp", 9999, system_u:object_r:this); 11 | } 12 | 13 | domain foo { 14 | allow(this, my_port, tcp_socket, node_bind); 15 | } 16 | -------------------------------------------------------------------------------- /data/policies/non_virtual_inherit.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | fn read(domain source) { 3 | allow(source, this, file, [read open getattr]); 4 | } 5 | } 6 | 7 | @derive(*,*) 8 | resource bar inherits foo {} 9 | 10 | domain baz { 11 | bar.read(); 12 | allow(this, foo, file, write); 13 | domain_transition(this, foo, some_other); 14 | 15 | fn reference_foo(domain some_other) { 16 | allow(some_other, foo, file, setattr); 17 | } 18 | } 19 | 20 | domain qux inherits baz { 21 | allow(this, foo, dir, write); 22 | } 23 | 24 | domain some_other { 25 | baz.reference_foo(this); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /data/policies/nonexistent_optional.cas: -------------------------------------------------------------------------------- 1 | domain bar { 2 | fn read(domain source) { 3 | allow(source, bar.blah, file, [read]); 4 | } 5 | 6 | resource blah {} 7 | } 8 | 9 | domain foo { 10 | // For 0.1, this is a warning that generates no CIL 11 | // Eventually it will be marked as optional 12 | doesnt_exist.read(); 13 | allow(this, resource, file, read); 14 | 15 | not_here.read(foo); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /data/policies/optional.cas: -------------------------------------------------------------------------------- 1 | resource foo {} 2 | resource bar {} 3 | 4 | domain baz { 5 | allow(this, foo, file, read); 6 | 7 | // In the current implementation, this does nothing 8 | // Eventually, the below rule(s) will be optional 9 | optional { 10 | allow(this, bar, file, write); 11 | } 12 | 13 | optional {} 14 | 15 | optional { 16 | allow(this, bar, file, getattr); 17 | allow(this, bar, dir, getattr); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /data/policies/permissions.cas: -------------------------------------------------------------------------------- 1 | resource bar {} 2 | 3 | domain foo { 4 | allow(foo, foo, capability, mac_override); 5 | allow(foo, foo, capability, [fowner wake_alarm]); 6 | allow(foo, bar, file, *); 7 | 8 | allow(foo, self, nscd, [ getpwd getgrp gethost ]); 9 | allow(foo, self, infiniband_endport, manage_subnet); 10 | 11 | // bpf is both a class and permission 12 | allow(this, self, capability, bpf); 13 | allow(this, self, bpf, map_create); 14 | allow(this, read, file, read); 15 | 16 | allow(this, read, file, some_perms); 17 | allow(this, read, file, some_perms_other_order); 18 | 19 | allow(this, bar, [file dir], [read write]); 20 | } 21 | 22 | resource read {} 23 | 24 | let some_perms = [ write read getattr ]; 25 | 26 | let some_perms_other_order = [ read write setattr ]; 27 | -------------------------------------------------------------------------------- /data/policies/resource_trans.cas: -------------------------------------------------------------------------------- 1 | resource foo { 2 | resource_transition(domain, bar, [file dir], this); 3 | 4 | fn foo_filetrans(domain source, resource parent_type, [class] classes) { 5 | resource_transition(source, parent_type, classes, this); 6 | } 7 | 8 | // Policies must include at least one av rule 9 | allow(domain, foo, file, [read]); 10 | } 11 | 12 | resource bar {} 13 | 14 | domain some_dom { 15 | foo.foo_filetrans(this, bar, [lnk_file]); 16 | } 17 | -------------------------------------------------------------------------------- /data/policies/self.cas: -------------------------------------------------------------------------------- 1 | domain qux { 2 | allow(this, self, file, read); 3 | } 4 | -------------------------------------------------------------------------------- /data/policies/simple.cas: -------------------------------------------------------------------------------- 1 | resource all_files {} 2 | 3 | domain all_processes { 4 | allow(all_processes, all_files, file, [read write open getattr append]); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /data/policies/stress/functions.cas: -------------------------------------------------------------------------------- 1 | // Stress Cascade by creating a lot of functions 2 | 3 | virtual resource foo { 4 | fn a(domain source) { 5 | allow(source, this, file, read); 6 | } 7 | 8 | fn b(domain source) { 9 | this.a(source); 10 | } 11 | fn c(domain source) { 12 | this.b(source); 13 | } 14 | fn d(domain source) { 15 | this.c(source); 16 | } 17 | fn e(domain source) { 18 | this.d(source); 19 | } 20 | fn f(domain source) { 21 | this.e(source); 22 | } 23 | fn g(domain source) { 24 | this.f(source); 25 | } 26 | fn h(domain source) { 27 | this.g(source); 28 | } 29 | fn i(domain source) { 30 | this.h(source); 31 | } 32 | fn j(domain source) { 33 | this.i(source); 34 | } 35 | fn k(domain source) { 36 | this.j(source); 37 | } 38 | fn l(domain source) { 39 | this.k(source); 40 | } 41 | fn m(domain source) { 42 | this.l(source); 43 | } 44 | fn n(domain source) { 45 | this.m(source); 46 | } 47 | fn o(domain source) { 48 | this.n(source); 49 | } 50 | fn p(domain source) { 51 | this.o(source); 52 | } 53 | fn q(domain source) { 54 | this.p(source); 55 | } 56 | fn r(domain source) { 57 | this.q(source); 58 | } 59 | fn s(domain source) { 60 | this.r(source); 61 | } 62 | fn t(domain source) { 63 | this.s(source); 64 | } 65 | fn u(domain source) { 66 | this.t(source); 67 | } 68 | fn v(domain source) { 69 | this.u(source); 70 | } 71 | fn w(domain source) { 72 | this.v(source); 73 | } 74 | fn x(domain source) { 75 | this.w(source); 76 | } 77 | fn y(domain source) { 78 | this.x(source); 79 | } 80 | fn z(domain source) { 81 | this.y(source); 82 | } 83 | } 84 | 85 | @derive(*,*) 86 | virtual resource r1 inherits foo {} 87 | @derive(*,*) 88 | virtual resource r2 inherits foo {} 89 | @derive(*,*) 90 | virtual resource r3 inherits foo {} 91 | @derive(*,*) 92 | virtual resource r4 inherits foo {} 93 | @derive(*,*) 94 | virtual resource r5 inherits foo {} 95 | @derive(*,*) 96 | virtual resource r6 inherits foo {} 97 | @derive(*,*) 98 | virtual resource r7 inherits foo {} 99 | @derive(*,*) 100 | virtual resource r8 inherits foo {} 101 | @derive(*,*) 102 | virtual resource r9 inherits foo {} 103 | @derive(*,*) 104 | virtual resource r10 inherits foo {} 105 | 106 | @associate([r1 r2 r3 r4 r5 r6 r7 r8 r9 r10]) 107 | domain bar { 108 | this.r10.z(); 109 | } 110 | 111 | @associate([r1 r2 r3 r4 r5 r6 r7 r8 r9 r10]) 112 | domain baz { 113 | this.r10.z(); 114 | } 115 | 116 | @associate([r1 r2 r3 r4 r5 r6 r7 r8 r9 r10]) 117 | domain qux { 118 | this.r10.z(); 119 | } 120 | 121 | @associate([r1 r2 r3 r4 r5 r6 r7 r8 r9 r10]) 122 | domain quux { 123 | this.r10.z(); 124 | } 125 | 126 | @associate([r1 r2 r3 r4 r5 r6 r7 r8 r9 r10]) 127 | virtual domain p1 {} 128 | 129 | virtual domain p2 inherits p1 {} 130 | virtual domain p3 inherits p2 {} 131 | virtual domain p4 inherits p3 {} 132 | virtual domain p5 inherits p4 {} 133 | virtual domain p6 inherits p5 {} 134 | virtual domain p7 inherits p6 {} 135 | virtual domain p8 inherits p7 {} 136 | virtual domain p9 inherits p8 {} 137 | domain child inherits p9 { 138 | this.r10.z(); 139 | } 140 | -------------------------------------------------------------------------------- /data/policies/this_casting.cas: -------------------------------------------------------------------------------- 1 | virtual resource virt_resource { 2 | fn read (domain source) { 3 | allow(source, this, file, [read]); 4 | } 5 | } 6 | 7 | virtual domain daemon { 8 | resource runtime inherits virt_resource {} 9 | 10 | fn daemon_read(domain source) { 11 | this.runtime.read(source); 12 | } 13 | } 14 | 15 | domain foo inherits daemon { 16 | // Policies must contain at least one AV rule 17 | allow(foo, resource, file, [read]); 18 | } -------------------------------------------------------------------------------- /data/policies/tmp_file.cas: -------------------------------------------------------------------------------- 1 | // This would go elsewhere in the real world, but including for sake of illustration here 2 | virtual resource file { 3 | fn dynamic_transition(domain source) { 4 | dynamic_transition(source, file); // TODO: file is overloaded right now 5 | } 6 | 7 | fn read(domain source) { 8 | allow(source, this, [file lnk_file], [read open getattr]); 9 | } 10 | 11 | // etc etc etc 12 | } 13 | 14 | @hint(denial, "To protect temp files, you may want to derive a child resource") 15 | @derive(manage, read, write, create) 16 | resource tmpfile inherits file, dir, lnk_file { 17 | 18 | // Define dynamic transition member function by deriving the union of parent class 19 | // dynamic_transition functions. You could also define your own. Since all three parents 20 | // define this function, it is an error to not derive or define it here 21 | @derive(dynamic_transition) 22 | // This is equivalent to: 23 | // dynamic_transition(source) { 24 | // file.dynamic_transition(source); 25 | // dir.dynamic_transition(source); 26 | // lnk_file.dynamic_transition(source); 27 | // } 28 | 29 | //@derive(all) 30 | // etc 31 | 32 | fn read(domain source) { 33 | file.read(); 34 | } 35 | 36 | // Derives are automatic unless the parents conflict. In this case, file, dir and lnk_file all provide these 37 | // So we need to explicitly opt in to our derivation. 38 | 39 | // When a domain associates with a tmpfile, it typically wants full control over it 40 | fn domain_association(domain source) { 41 | this.manage(source); 42 | 43 | this.dynamic_transition(source, tmp); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /data/policies/trait.cas: -------------------------------------------------------------------------------- 1 | @alias(trait_alias) 2 | trait resource my_trait { 3 | fn write(domain source) { 4 | allow(source, this, file, write); 5 | } 6 | } 7 | 8 | resource foo inherits my_trait { 9 | fn write(domain source) { 10 | allow(source, this, dir, write); 11 | } 12 | } 13 | 14 | @derive([write], parents=*) 15 | resource baz inherits my_trait {} 16 | 17 | resource qux inherits trait_alias {} 18 | 19 | domain bar { 20 | foo.write(this); 21 | baz.write(this); 22 | qux.write(this); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /data/policies/virtual_function.cas: -------------------------------------------------------------------------------- 1 | virtual resource foo_parent { 2 | virtual fn foo(domain source) {} 3 | } 4 | 5 | resource foo inherits foo_parent { 6 | fn foo(domain source) { 7 | allow(source, this, file, read); 8 | } 9 | } 10 | 11 | domain bar { 12 | foo.foo(this); 13 | } 14 | -------------------------------------------------------------------------------- /doc/policy_composer.md: -------------------------------------------------------------------------------- 1 | # Modules 2 | Modules are made up of domains, resources, traits and other modules. They are 3 | the building blocks for larger scale policy and are a core component of a Machine. 4 | 5 | The user can declare multiple modules and create machines from these modules. 6 | Modules can have alias annotations to provide an alternate name. The user can 7 | then decide which of these individual modules or machines to build (Building of 8 | individual modules and machines is not yet supported. See ROADMAP.md for more 9 | information). A module can also be made virtual, in which case it cannot be 10 | built. The purpose of a virtual module is to create other modules from it. 11 | Since virtual modules cannot have a real instantiation, they are ineligible to 12 | be compile targets. The addition of modules not only simplifies the process of 13 | policy building for users, but also allows for customizability and scalability. 14 | 15 | Example module declaration: 16 | 17 | ``` 18 | module my_module { 19 | domain foo; 20 | domain bar; 21 | module other_module; 22 | } 23 | ``` 24 | 25 | # Machines 26 | Machines contain modules and configuration options. Some configurations are 27 | mandatory, meaning that they must be explicitly included in a machine, while 28 | other are optional. If the user does not include the optional configurations in 29 | a machine, the default values will be given. 30 | 31 | Configuration | Mandatory? | Description 32 | --------------|------------|------------- 33 | handle_unknown_perms|Yes|This is how permissions missing from the policy will be handled when loading the policy. The options are allow, deny, and reject. [handleunknown](https://github.com/SELinuxProject/selinux/blob/master/secilc/docs/cil_policy_config_statements.md#handleunknown) 34 | machine_type|No|This controls whether MLS/MCS controls are enabled. The only currently supported option is standard. Options for mls and mcs will be added in the future. The default is standard. [MLS/MCS](https://github.com/SELinuxProject/selinux-notebook/blob/main/src/mls_mcs.md) 35 | monolithic|No|This is the option for choosing to build monolithic or modular policy. The options are true and false, with true being for a monolithic build and false for modular. The default is true. [Monolithic and Modular policies](https://github.com/SELinuxProject/selinux-notebook/blob/main/src/types_of_policy.md#monolithic-policy) 36 | 37 | Example machine declaration: 38 | 39 | ``` 40 | machine my_machine { 41 | module my_module; 42 | module some_other_module; 43 | let handle_unknown_perms = "allow"; 44 | } 45 | ``` 46 | 47 | # Machine Building 48 | There are 3 options for machine building. 49 | 50 | (1) compile_combined 51 | This is the default when casc is ran without the -s flag. It compiles all 52 | defined policies into a single, combined policy. This policy is then outputted 53 | into a file named "out.cil" unless another filename is specified using the -o 54 | flag. 55 | 56 | (2) compile_machine_policies 57 | This builds the machine(s) that the user chooses by supplying the names of those 58 | machines after the -s flag is used. The CIL policy for each individual machine is 59 | outputted into a file named "\.cil". 60 | 61 | (3) compile_machine_policies_all 62 | This builds all of the machines if the -s flag is used followed by "all". 63 | The CIL policy for each individual machine is outputted into a file named "\.cil". 64 | 65 | ## Functions 66 | Currently, if the machine to build contains a call to a function that exists in 67 | the policy, but is not in the machine, there will be a "No such function" 68 | compile error. This is because during compilation, the function map is reduced 69 | to only the functions within the particular machine. 70 | -------------------------------------------------------------------------------- /doc/resource_association.md: -------------------------------------------------------------------------------- 1 | # Resource Association 2 | Resource association is a new feature in Cascade to make working with resources 3 | that logically correspond to a domain easier. Domains often have certain 4 | resource types, traditionally with common suffixes such as `[domain]_tmp_t`, 5 | `[domain]_conf_t` etc. These types are conventionally refered to as "derived 6 | types" and typically have similar access patterns resulting in large amounts of 7 | highly similar redundant policy. Automatically handling these scenarios 8 | reduces effort and risk of mistakes in policy writing. 9 | 10 | ## Example: tmp files 11 | Many processes write files in /tmp. A common pattern in refpolicy style 12 | policies is to create a `[domain]_tmp_t` type, associate it with the `tmp_file` 13 | attribute, set up type transitions so that new files in /tmp will get the new 14 | label and allow the parent domain `manage` access on it. If any other domains 15 | require access to the derived type, interfaces must be created in the 16 | associated `.if` file. 17 | 18 | As an example, one can look at the iptables module in refpolicy [1]. The 19 | domain is first created and associated with the attribute: 20 | 21 | ``` 22 | type iptables_tmp_t; 23 | files_tmp_file(iptables_tmp_t) 24 | ``` 25 | 26 | Later in the file, access is granted on files and directories, and a type 27 | transition is set up: 28 | 29 | ``` 30 | allow iptables_t iptables_tmp_t:dir manage_dir_perms; 31 | allow iptables_t iptables_tmp_t:file manage_file_perms; 32 | files_tmp_filetrans(iptables_t, iptables_tmp_t, { file dir }) 33 | ``` 34 | 35 | In this instance, no interfaces have been defined in iptables.if for 36 | `iptables_tmp_t`. If another domain required access to `iptables_tmp_t`, 37 | those interfaces would need to be manually defined in order to maintain proper 38 | encapsulation. 39 | 40 | ### Potential problems with the traditional approach 41 | The above described scheme has many numerous possible failures. Five different 42 | sorts of rules are required for proper functionality and all must be manually 43 | written, with omissions potentially resulting in functional issues that may 44 | only be obvious under rigorous testing. The possible object classes these 45 | files will be must be enumerated by the policy developer, who may forget less 46 | common use cases such as directories or symlinks, even when they are desired. 47 | Interface names are typically very standard (`domainname_read_tmp_files()`), 48 | but require several lines of boilerplate to define, an exercise typically done 49 | on an ad hoc as needed basis. 50 | 51 | ## Tmp files in Cascade with resource association 52 | Using resource association, we write all of the repeated access as part of the 53 | `tmp_files` virtual resource (equivalent in this case to a refpolicy attribute), 54 | inherit the full functionality in child types, and then associate them with a 55 | domain to automatically handle all of the mapping between domain and access to 56 | a particular resource. These associated resources can then automatically be 57 | carried to children, covering a use case of templates in a more readable and 58 | scalable manner. 59 | 60 | In tmp.cas: 61 | 62 | ``` 63 | resource tmp_file { 64 | // all common functions go here 65 | // In practice, tmp_file may inherit from other domains providing many standard functions 66 | fn read_files(domain source) { 67 | allow(source, this, file, read); 68 | } 69 | // etc 70 | 71 | @associated_call 72 | fn associate_tmp_files(domain source) { 73 | this.manage_files(source); 74 | this.manage_dirs(source); 75 | this.manage_symlinks(source); 76 | resource_transition(this, source, tmp_file, [file dir symlink]); 77 | } 78 | } 79 | ``` 80 | 81 | In iptables.cas: 82 | 83 | ``` 84 | // Note that in the common case, iptables_tmp needs no special rules 85 | resource iptables_tmp inherits tmp_file {} 86 | 87 | // Sets up all of the items discussed above based on the implementation of tmp_file as inherited in iptables_tmp 88 | @associate([iptables_tmp]) 89 | domain iptables {} 90 | ``` 91 | 92 | ## Inheriting association 93 | When a domain inherits from another domain, new resources that inherit from 94 | associated resources are automatically created. This enables a pattern of 95 | setting up a group of domains with common access patterns and deriving specific 96 | instances. Using this pattern in refpolicy requires templates, and an example 97 | can be found in the `qemu_domain_template()` policy [2]. In order to implement 98 | similar functionality in Cascade, a developer would write policy like this: 99 | 100 | ``` 101 | virtual resource qemu_tmp inherits tmp_file {} 102 | 103 | @associate([qemu_tmp]) 104 | virtual domain qemu { 105 | // other generic qemu domain policy 106 | } 107 | 108 | domain some_qemu_domain inherits qemu {} 109 | ``` 110 | 111 | In this case, a resource inheriting from `qemu_tmp` is automatically created, 112 | named `some_qemu_domain.qemu_tmp`, equivalent to the following definition: 113 | 114 | ``` 115 | resource some_qemu_domain.qemu_tmp inherits qemu_tmp {} 116 | ``` 117 | 118 | (Although note that explicit declaration of a type containing the character "." 119 | is disallowed). 120 | 121 | The `associate_tmp_files()` call will also automatically be performed as part 122 | of the inheritance from `qemu`. 123 | 124 | Note that a consequence of this is that the "this" keyword in the original 125 | definition may refer to an automatically created type (some_qemu_domain.qemu_tmp 126 | in this example). This means that there is a distinction between using the type 127 | name (where it will always refer to the original type) and the keyword "this" 128 | (which will refer to a specific instantiation such as an inherited or 129 | automatically created type). 130 | 131 | ## Nested syntax 132 | Assocations can also be created via a nesting syntax, like below: 133 | 134 | ``` 135 | domain bar { 136 | resource foo {} 137 | } 138 | ``` 139 | 140 | This has two key differences from the annotation style association. 141 | 142 | 1. It does not require there to be a global type. In the above example no 143 | global `foo` exists. Any of `bar.foo`'s parents need to be inherited 144 | explicitly 145 | 2. This allows you to refer to `bar.foo` directly as `foo` inside of the `bar` 146 | block. 147 | 148 | [1] https://github.com/SELinuxProject/refpolicy/blob/master/policy/modules/system/iptables.te 149 | https://github.com/SELinuxProject/refpolicy/blob/master/policy/modules/system/iptables.if 150 | 151 | [2] https://github.com/SELinuxProject/refpolicy/blob/master/policy/modules/apps/qemu.if 152 | -------------------------------------------------------------------------------- /owners.txt: -------------------------------------------------------------------------------- 1 | *jamorris 2 | *paulmoore 3 | chpebeni 4 | daburgen 5 | misalaun 6 | -------------------------------------------------------------------------------- /src/alias_map.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | use std::collections::{BTreeMap, BTreeSet}; 4 | use std::ops::Range; 5 | 6 | use codespan_reporting::files::SimpleFile; 7 | 8 | use crate::ast::CascadeString; 9 | use crate::error::{CascadeErrors, ErrorItem, InternalError}; 10 | use crate::util::append_set_map; 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct AliasMap { 14 | declarations: BTreeMap, 15 | aliases: BTreeMap, 16 | // Secondary_indices allow efficient lookups based on some other key. This is useful to 17 | // efficiently get a subset of the map based on a prepopulated key, such as all functions for a 18 | // given type 19 | secondary_indices: BTreeMap>, 20 | } 21 | 22 | pub type AliasMapIter<'a, T> = std::collections::btree_map::Iter<'a, String, T>; 23 | pub type AliasMapValues<'a, T> = std::collections::btree_map::Values<'a, String, T>; 24 | pub type AliasMapValuesMut<'a, T> = std::collections::btree_map::ValuesMut<'a, String, T>; 25 | pub type AliasMapIntoIter = std::collections::btree_map::IntoIter; 26 | 27 | impl AliasMap { 28 | fn get_type_name<'a>(aliases: &'a BTreeMap, key: &'a str) -> &'a str { 29 | if aliases.contains_key(&CascadeString::from(key)) { 30 | &aliases[&CascadeString::from(key)] 31 | } else { 32 | key 33 | } 34 | } 35 | 36 | pub fn get(&self, key: &str) -> Option<&T> { 37 | let type_name = Self::get_type_name(&self.aliases, key); 38 | self.declarations.get(type_name) 39 | } 40 | 41 | pub fn get_mut(&mut self, key: &str) -> Option<&mut T> { 42 | let type_name = Self::get_type_name(&self.aliases, key); 43 | self.declarations.get_mut(type_name) 44 | } 45 | 46 | pub fn new() -> Self { 47 | AliasMap { 48 | declarations: BTreeMap::new(), 49 | aliases: BTreeMap::new(), 50 | secondary_indices: BTreeMap::new(), 51 | } 52 | } 53 | 54 | pub fn insert(&mut self, key: String, value: T) -> Result<(), CascadeErrors> { 55 | // try_insert() is nightly only. Convert once stable. 56 | if let Some(orig_decl) = self.get(&key) { 57 | // If the file is None, this is a synthetic type, and we should have handled 58 | // the error earlier. 59 | let mut error = ErrorItem::make_compile_or_internal_error( 60 | "Duplicate declaration", 61 | value.get_file().as_ref(), 62 | value.get_name_range(), 63 | &format!( 64 | "A {} named {} already exists", 65 | value.get_generic_name(), 66 | key 67 | ), 68 | ); 69 | if let ErrorItem::Compile(e) = error { 70 | let (file, range) = match (orig_decl.get_file(), orig_decl.get_name_range()) { 71 | (Some(file), Some(range)) => (file, range), 72 | _ => { 73 | // The previous one was a synthetic type. We should have already errored 74 | // out 75 | return Err(ErrorItem::Internal(InternalError::new()).into()); 76 | } 77 | }; 78 | error = ErrorItem::Compile(e.add_additional_message( 79 | &file, 80 | range, 81 | "Already defined here", 82 | )); 83 | } 84 | return Err(error.into()); 85 | } 86 | 87 | for index in value.get_secondary_indices() { 88 | match self.secondary_indices.get_mut(&index) { 89 | Some(val) => { 90 | val.insert(key.clone()); 91 | } 92 | None => { 93 | self.secondary_indices 94 | .insert(index, BTreeSet::from([key.clone()])); 95 | } 96 | } 97 | } 98 | self.declarations.insert(key, value); 99 | Ok(()) 100 | } 101 | 102 | pub fn values(&self) -> AliasMapValues<'_, T> { 103 | self.declarations.values() 104 | } 105 | 106 | pub fn values_mut(&mut self) -> AliasMapValuesMut<'_, T> { 107 | self.declarations.values_mut() 108 | } 109 | 110 | pub fn values_by_index(&self, index: String) -> Vec<&T> { 111 | if let Some(secondary) = self.secondary_indices.get(&index) { 112 | secondary.iter().filter_map(|v| self.get(v)).collect() 113 | } else { 114 | Vec::new() 115 | } 116 | } 117 | 118 | pub fn iter(&self) -> AliasMapIter<'_, T> { 119 | self.declarations.iter() 120 | } 121 | 122 | pub fn append(&mut self, other: &mut AliasMap) { 123 | self.declarations.append(&mut other.declarations); 124 | self.aliases.append(&mut other.aliases); 125 | append_set_map(&mut self.secondary_indices, &mut other.secondary_indices); 126 | } 127 | 128 | pub fn validate_aliases( 129 | &self, 130 | aliases: &BTreeMap, 131 | alias_files: &BTreeMap>, 132 | ) -> Result<(), CascadeErrors> { 133 | let mut errors = CascadeErrors::new(); 134 | for a in aliases.keys() { 135 | if let Some(existing) = self.declarations.get(a.as_ref()) { 136 | errors.append( 137 | ErrorItem::make_compile_or_internal_error( 138 | &format!( 139 | "Alias name conflicts with an existing {}", 140 | existing.get_generic_name() 141 | ), 142 | alias_files.get(a), 143 | a.get_range(), 144 | "", 145 | ) 146 | .maybe_add_additional_message( 147 | existing.get_file().as_ref(), 148 | existing.get_name_range(), 149 | &format!("Existing {} found here", existing.get_generic_name()), 150 | ) 151 | .into(), 152 | ) 153 | } 154 | } 155 | errors.into_result(()) 156 | } 157 | 158 | pub fn set_aliases(&mut self, aliases: BTreeMap) { 159 | self.update_alias_secondary_indices(&aliases); 160 | self.aliases = aliases; 161 | } 162 | 163 | fn update_alias_secondary_indices(&mut self, new_aliases: &BTreeMap) { 164 | for (alias, true_name) in new_aliases { 165 | if let Some(val) = self.get(true_name) { 166 | for secondary in val.get_secondary_indices() { 167 | match self.secondary_indices.get_mut(&secondary) { 168 | Some(val) => { 169 | val.insert(alias.to_string()); 170 | } 171 | None => { 172 | self.secondary_indices 173 | .insert(secondary, BTreeSet::from([alias.to_string()])); 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | // Add a single alias 182 | pub fn add_alias(&mut self, alias: CascadeString, true_name: String) { 183 | let mut map = BTreeMap::new(); 184 | // TODO: These clones can probably be eliminated, but it's not immediately clear to me how 185 | map.insert(alias.clone(), true_name.clone()); 186 | self.update_alias_secondary_indices(&map); 187 | // TODO: worry about duplicates 188 | self.aliases.insert(alias, true_name); 189 | } 190 | 191 | // fallible extend, reject duplicates 192 | pub fn try_extend>( 193 | &mut self, 194 | iter: I, 195 | ) -> Result<(), CascadeErrors> { 196 | for item in iter { 197 | self.insert(item.0, item.1)?; 198 | } 199 | Ok(()) 200 | } 201 | } 202 | 203 | impl Extend<(String, T)> for AliasMap { 204 | fn extend>(&mut self, iter: I) { 205 | self.declarations.extend(iter) 206 | } 207 | } 208 | 209 | impl IntoIterator for AliasMap { 210 | type Item = (String, T); 211 | type IntoIter = AliasMapIntoIter; 212 | 213 | fn into_iter(self) -> AliasMapIntoIter { 214 | self.declarations.into_iter() 215 | } 216 | } 217 | 218 | pub trait Declared { 219 | fn get_file(&self) -> Option>; 220 | fn get_name_range(&self) -> Option>; 221 | fn get_generic_name(&self) -> String; 222 | // Get a list of values to access this via secondary index. 223 | fn get_secondary_indices(&self) -> Vec; 224 | } 225 | -------------------------------------------------------------------------------- /src/annotations.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::collections::{BTreeMap, BTreeSet}; 3 | use std::ops::Range; 4 | 5 | use codespan_reporting::files::SimpleFile; 6 | 7 | use crate::ast::{Annotation, Annotations, Argument, CascadeString}; 8 | use crate::error::ErrorItem; 9 | use crate::warning::{Warning, Warnings, WithWarnings}; 10 | 11 | #[derive(Clone, Debug, Eq, PartialEq)] 12 | pub struct AssociatedResource { 13 | name: CascadeString, 14 | doms: BTreeSet>, 15 | ranges: BTreeMap>, 16 | } 17 | 18 | impl AssociatedResource { 19 | // Unlike most get_range() functions, this one takes an argument. An AssociatedResource 20 | // possibly contains information about various associated points, so we need to know the name 21 | // of the resource we want the range for 22 | pub fn get_range(&self, resource_name: &str) -> Option> { 23 | self.ranges.get(resource_name).cloned() 24 | } 25 | 26 | pub fn name(&self) -> &CascadeString { 27 | &self.name 28 | } 29 | 30 | pub fn get_class_names(&self) -> Vec { 31 | self.doms 32 | .iter() 33 | .map(|d| match d { 34 | Some(d) => { 35 | format!("{}.{}", d, &self.name) 36 | } 37 | None => self.name.to_string(), 38 | }) 39 | .collect() 40 | } 41 | 42 | pub fn basename(&self) -> &str { 43 | self.name.as_ref() 44 | } 45 | 46 | // Return true if type_name is one of the resources that have been combined in this 47 | // AssociatedResource 48 | pub fn string_is_instance(&self, type_name: &CascadeString) -> bool { 49 | match type_name.as_ref().split_once('.') { 50 | Some((dom, res)) => { 51 | res == self.name && self.doms.contains(&Some(CascadeString::from(dom))) 52 | } 53 | None => type_name == &self.name && self.doms.contains(&None), 54 | } 55 | } 56 | } 57 | 58 | impl From<&CascadeString> for AssociatedResource { 59 | fn from(cs: &CascadeString) -> Self { 60 | let mut ranges = BTreeMap::new(); 61 | // If the range is None, we just don't store it and later map lookups will return None, 62 | // which is exactly what we want 63 | if let Some(range) = cs.get_range() { 64 | ranges.insert(cs.to_string(), range); 65 | } 66 | 67 | match cs.as_ref().split_once('.') { 68 | Some((dom, res)) => AssociatedResource { 69 | name: res.into(), 70 | doms: [Some(dom.into())].into(), 71 | ranges, 72 | }, 73 | None => AssociatedResource { 74 | name: cs.clone(), 75 | doms: [None].into(), 76 | ranges, 77 | }, 78 | } 79 | } 80 | } 81 | 82 | impl From for AssociatedResource { 83 | fn from(cs: CascadeString) -> Self { 84 | (&cs).into() 85 | } 86 | } 87 | 88 | impl PartialOrd for AssociatedResource { 89 | fn partial_cmp(&self, other: &Self) -> Option { 90 | Some(self.cmp(other)) 91 | } 92 | } 93 | 94 | impl Ord for AssociatedResource { 95 | fn cmp(&self, other: &Self) -> Ordering { 96 | self.name.cmp(&other.name) 97 | } 98 | } 99 | 100 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 101 | pub struct Associated { 102 | pub resources: BTreeSet, 103 | } 104 | 105 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 106 | pub enum InsertExtendTiming { 107 | All, 108 | Early, 109 | Late, 110 | } 111 | 112 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 113 | pub enum AnnotationInfo { 114 | MakeList, 115 | Associate(Associated), 116 | NestAssociate(Associated), 117 | Alias(CascadeString), 118 | // Inherit isn't exposed to users, who should use the "inherits type" syntax, but its helpful 119 | // internally to track inherits on extends as annotations 120 | Inherit(Vec), 121 | Derive(Vec), 122 | NoDerive, 123 | } 124 | 125 | impl AnnotationInfo { 126 | // All data should exactly break into three sets: a.difference(b), b.difference(a) and 127 | // a.intersection(b) (which is equivalent to b.intersection(a)) 128 | 129 | // Returns a single AnnotationInfo containing any overlap, if it exists 130 | pub fn intersection(&self, other: &AnnotationInfo) -> Option { 131 | use AnnotationInfo::*; 132 | match (self, other) { 133 | (MakeList, MakeList) => Some(AnnotationInfo::MakeList), 134 | (NoDerive, NoDerive) => Some(AnnotationInfo::NoDerive), 135 | (Associate(left), Associate(right)) | (NestAssociate(left), NestAssociate(right)) => { 136 | let mut intersect: BTreeSet = BTreeSet::new(); 137 | for l_res in &left.resources { 138 | for r_res in &right.resources { 139 | if l_res.name == r_res.name { 140 | // TODO: The whole below should probably be in an impl in 141 | // AssociatedResource. That allows at least ranges to become private 142 | let mut unioned_ranges = BTreeMap::new(); 143 | for (key, val) in &l_res.ranges { 144 | if r_res.ranges.contains_key(key as &String) { 145 | // TODO: I think this could result in weird error messages. 146 | // We're just keeping the left and discarding the right. I'm 147 | // not 100% sure how much that matters, but if there's 148 | // something wrong with right and not left, the error would be 149 | // confusing. Probably the common case is just "there is a 150 | // parent named this", and so it doesn't overly matter if we 151 | // point at right or left... 152 | unioned_ranges.insert(key.to_string(), val.clone()); 153 | } 154 | } 155 | // TODO: Do we need to worry about insert failing? 156 | intersect.insert(AssociatedResource { 157 | name: l_res.name.clone(), 158 | doms: l_res.doms.union(&r_res.doms).cloned().collect(), 159 | ranges: unioned_ranges, 160 | }); 161 | } 162 | } 163 | } 164 | if intersect.is_empty() { 165 | None 166 | } else { 167 | match self { 168 | Associate(_) => Some(Associate(Associated { 169 | resources: intersect, 170 | })), 171 | NestAssociate(_) => Some(NestAssociate(Associated { 172 | resources: intersect, 173 | })), 174 | _ => { 175 | // impossible 176 | None 177 | } 178 | } 179 | } 180 | } 181 | (Alias(left), Alias(right)) => { 182 | if left == right { 183 | Some(Alias(left.clone())) 184 | } else { 185 | None 186 | } 187 | } 188 | // Treat all @derives as unique, because they require special processing later 189 | (Derive(_), Derive(_)) => None, 190 | // These should be filtered earlier and never processed here 191 | (Inherit(_), Inherit(_)) => None, 192 | // Enumerate the non-equal cases explicitly so that we get non-exhaustive match errors 193 | // when updating the enum 194 | (MakeList, _) 195 | | (Associate(_), _) 196 | | (NestAssociate(_), _) 197 | | (Alias(_), _) 198 | | (Inherit(_), _) 199 | | (Derive(_), _) 200 | | (NoDerive, _) => None, 201 | } 202 | } 203 | 204 | // Returns an AnnotationInfo with only the portion in self but not other. 205 | pub fn difference(&self, other: &AnnotationInfo) -> Option { 206 | use AnnotationInfo::*; 207 | match (self, other) { 208 | (MakeList, MakeList) => None, 209 | (NoDerive, NoDerive) => None, 210 | (Associate(left), Associate(right)) | (NestAssociate(left), NestAssociate(right)) => { 211 | let difference: BTreeSet = left 212 | .resources 213 | .iter() 214 | .filter(|l_res| !right.resources.iter().any(|r_res| r_res.name == l_res.name)) 215 | .cloned() 216 | .collect(); 217 | 218 | if difference.is_empty() { 219 | None 220 | } else { 221 | match self { 222 | Associate(_) => Some(Associate(Associated { 223 | resources: difference, 224 | })), 225 | NestAssociate(_) => Some(NestAssociate(Associated { 226 | resources: difference, 227 | })), 228 | _ => { 229 | //impossible 230 | None 231 | } 232 | } 233 | } 234 | } 235 | (Alias(left), Alias(right)) => { 236 | if left == right { 237 | None 238 | } else { 239 | Some(Alias(left.clone())) 240 | } 241 | } 242 | // No need to special handle Derive/Derive. Derives are always considered disjoint 243 | (Derive(_), _) 244 | | (MakeList, _) 245 | | (Associate(_), _) 246 | | (NestAssociate(_), _) 247 | | (Alias(_), _) 248 | | (NoDerive, _) 249 | | (Inherit(_), _) => Some(self.clone()), 250 | } 251 | } 252 | 253 | pub fn insert_timing(&self) -> InsertExtendTiming { 254 | match self { 255 | AnnotationInfo::Associate(_) => InsertExtendTiming::All, 256 | AnnotationInfo::NestAssociate(_) => InsertExtendTiming::Early, 257 | // Inherit is Early, but note that it may also be set on an associated resource, in 258 | // which case it also has special handling in create_synthetic resource. The "Early" 259 | // handling handles regular types 260 | AnnotationInfo::Inherit(_) => InsertExtendTiming::Early, 261 | AnnotationInfo::Derive(_) => InsertExtendTiming::Late, 262 | AnnotationInfo::NoDerive => InsertExtendTiming::Late, 263 | AnnotationInfo::MakeList => InsertExtendTiming::Late, 264 | AnnotationInfo::Alias(_) => InsertExtendTiming::Late, 265 | } 266 | } 267 | 268 | pub fn as_inherit(&self) -> Option<&Vec> { 269 | if let AnnotationInfo::Inherit(v) = self { 270 | Some(v) 271 | } else { 272 | None 273 | } 274 | } 275 | } 276 | 277 | pub trait Annotated { 278 | fn get_annotations(&self) -> std::collections::btree_set::Iter; 279 | } 280 | 281 | fn get_associate( 282 | file: &SimpleFile, 283 | annotation_name_range: Option>, 284 | annotation: &Annotation, 285 | ) -> Result { 286 | let mut args = annotation.arguments.iter(); 287 | 288 | let res_list = match args.next() { 289 | None => { 290 | return Err(ErrorItem::make_compile_or_internal_error( 291 | "Missing resource list as first argument", 292 | Some(file), 293 | annotation_name_range, 294 | "You must use a set of resource names, enclosed by square brackets, as first argument.", 295 | )); 296 | } 297 | Some(Argument::List(l)) => l, 298 | Some(a) => { 299 | return Err(ErrorItem::make_compile_or_internal_error( 300 | "Invalid argument type", 301 | Some(file), 302 | a.get_range(), 303 | "You must use a set of resource names, enclosed by square brackets, as first argument.", 304 | )); 305 | } 306 | }; 307 | 308 | if let Some(a) = args.next() { 309 | return Err(ErrorItem::make_compile_or_internal_error( 310 | "Superfluous argument", 311 | Some(file), 312 | a.get_range(), 313 | "There must be only one argument.", 314 | )); 315 | } 316 | 317 | Ok(AnnotationInfo::Associate(Associated { 318 | // Checks for duplicate resources. 319 | resources: res_list.iter().try_fold(BTreeSet::new(), |mut s, e| { 320 | if !s.insert(e.into()) { 321 | Err(ErrorItem::make_compile_or_internal_error( 322 | "Duplicate resource", 323 | Some(file), 324 | e.get_range(), 325 | "Only unique resource names are valid.", 326 | )) 327 | } else { 328 | Ok(s) 329 | } 330 | })?, 331 | })) 332 | } 333 | 334 | pub fn get_type_annotations( 335 | file: &SimpleFile, 336 | annotations: &Annotations, 337 | ) -> Result>, ErrorItem> { 338 | let mut infos = BTreeSet::new(); 339 | let mut warnings = Warnings::new(); 340 | 341 | // Only allow a set of specific annotation names and strictly check their arguments. 342 | // TODO: Add tests to verify these checks. 343 | for annotation in annotations.annotations.iter() { 344 | match annotation.name.as_ref() { 345 | "makelist" => { 346 | // TODO: Check arguments 347 | // Multiple @makelist annotations doesn't make sense. 348 | if !infos.insert(AnnotationInfo::MakeList) { 349 | return Err(ErrorItem::make_compile_or_internal_error( 350 | "Multiple @makelist annotations", 351 | Some(file), 352 | annotation.name.get_range(), 353 | "You need to remove duplicated @makelist annotations.", 354 | )); 355 | } 356 | } 357 | "associate" => { 358 | // Multiple @associate annotations doesn't make sense. 359 | if !infos.insert(get_associate( 360 | file, 361 | annotation.name.get_range(), 362 | annotation, 363 | )?) { 364 | return Err(ErrorItem::make_compile_or_internal_error( 365 | "Multiple @associate annotations", 366 | Some(file), 367 | annotation.name.get_range(), 368 | "You need to remove duplicated @associate annotations.", 369 | )); 370 | } 371 | } 372 | "alias" => { 373 | for a in &annotation.arguments { 374 | match a { 375 | Argument::Var(a) => { 376 | infos.insert(AnnotationInfo::Alias(a.clone())); 377 | } 378 | _ => { 379 | return Err(ErrorItem::make_compile_or_internal_error( 380 | "Invalid alias", 381 | Some(file), 382 | a.get_range(), 383 | "This must be a symbol", 384 | )); 385 | } 386 | } 387 | } 388 | } 389 | "derive" => { 390 | // Arguments are validated at function creation time 391 | infos.insert(AnnotationInfo::Derive(annotation.arguments.clone())); 392 | } 393 | "noderive" => { 394 | // Do not implicit derive for this type 395 | infos.insert(AnnotationInfo::NoDerive); 396 | } 397 | "hint" => { 398 | // If get_range() is none, we generated a synthetic hint. This could be because of 399 | // inheritance, in which case there was a warning on the parent. Otherwise, if we 400 | // generated a synthetic hint for some reason, we can always generate it 401 | // differently if the signature changes, and the point of the warning is to not 402 | // rely on any existing signature. So a warning is only necessary if the hint is 403 | // actually in source. 404 | if let Some(range) = annotation.name.get_range() { 405 | warnings.push(Warning::new("The hint annotation is not yet supported", 406 | file, 407 | range, 408 | "The signature expected by this annotation may change without warning, and it is currently not functional.")); 409 | } 410 | } 411 | _ => { 412 | return Err(ErrorItem::make_compile_or_internal_error( 413 | "Unknown annotation", 414 | Some(file), 415 | annotation.name.get_range(), 416 | "This is not a valid annotation name.", 417 | )); 418 | } 419 | } 420 | } 421 | Ok(WithWarnings::new(infos, warnings)) 422 | } 423 | 424 | #[cfg(test)] 425 | mod tests { 426 | use super::*; 427 | 428 | #[test] 429 | fn ar_string_is_instance_test() { 430 | let foo_bar = CascadeString::from("foo.bar"); 431 | let bar = CascadeString::from("bar"); 432 | let foo = CascadeString::from("foo"); 433 | let ar = AssociatedResource::from(&foo_bar); 434 | 435 | assert!(ar.string_is_instance(&foo_bar)); 436 | assert!(!ar.string_is_instance(&bar)); 437 | assert!(!ar.string_is_instance(&foo)); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /src/bin/audit2cascade.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | #![allow(clippy::manual_flatten)] 4 | #![allow(clippy::new_without_default)] 5 | fn main() { 6 | println!("Hello World!"); 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/casc/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug)] 4 | #[clap( 5 | author, 6 | version, 7 | name = "casc", 8 | about = "Compile Cascade SELinux policies into CIL", 9 | long_about = "Compile Cascade SELinux policies into CIL. 10 | 11 | The -o option to combine all policies and the -s option to build individual machines are mutually exclusive. See the OPTIONS section for more information on these options." 12 | )] 13 | pub struct Args { 14 | /// List of input files to process. Directories are searched recursively. 15 | #[clap(required(true))] 16 | pub input_file: Vec, 17 | /// This is the default behavior. 18 | /// Combine all policies into a monolithic machine policy with machine configuration options set to default values. 19 | /// The generated CIL file is named OUT_FILENAME. 20 | #[clap(default_value = "out.cil", short, value_parser = clap::builder::ValueParser::new(parse_out_filename))] 21 | pub out_filename: String, 22 | /// Build the machines from the MACHINE_NAMES list. "-m all" to build all defined machines. 23 | #[clap(short, conflicts_with = "out_filename")] 24 | pub machine_names: Vec, 25 | ///colorize the output. WHEN can be 'always', 'auto' (default), or 'never' 26 | #[clap(long, value_enum, id = "WHEN")] 27 | pub color: Option, 28 | ///Compile the generated CIL file into policy and generate a tar.gz putting policy files in the 29 | ///correct paths 30 | #[clap(long)] 31 | pub package: bool, 32 | } 33 | 34 | fn parse_out_filename(filename: &str) -> Result { 35 | if filename.ends_with(".cil") { 36 | return Ok(filename.to_string()); 37 | } 38 | Err(String::from("The value does not end in \".cil\"")) 39 | } 40 | 41 | #[derive(clap::ValueEnum, Clone, Debug)] 42 | pub enum ColorArg { 43 | Always, 44 | Auto, 45 | Never, 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | use clap::CommandFactory; 52 | 53 | #[test] 54 | fn test_cli() { 55 | Args::command().debug_assert(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/bin/casc/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | #![allow(clippy::manual_flatten)] 4 | #![allow(clippy::new_without_default)] 5 | use selinux_cascade::error::{CascadeErrors, ErrorItem}; 6 | use selinux_cascade::{compile_combined, compile_machine_policies, compile_machine_policies_all}; 7 | 8 | mod args; 9 | mod package; 10 | use args::{Args, ColorArg}; 11 | use package::build_package; 12 | 13 | use clap::Parser; 14 | use is_terminal::IsTerminal; 15 | use std::collections::HashMap; 16 | use std::fs::File; 17 | use std::io::{Error, ErrorKind, Write}; 18 | use termcolor::ColorChoice; 19 | use walkdir::WalkDir; 20 | 21 | fn main() -> std::io::Result<()> { 22 | let args = Args::parse(); 23 | let policies: Vec = match get_policy_files(args.input_file) { 24 | Ok(mut s) => { 25 | // Always treat files in the same order for determinism in compilation 26 | // sort_unstable() does not preserve equality, which is fine because two 27 | // different files cannot have the same relative path 28 | s.sort_unstable(); 29 | s 30 | } 31 | Err(e) => { 32 | eprintln!("{e}"); 33 | return Err(e); 34 | } 35 | }; 36 | if policies.is_empty() { 37 | // Files supplied on command line, but no .cas files found 38 | return Err(Error::new( 39 | ErrorKind::InvalidData, 40 | "No policy source files found", 41 | )); 42 | } 43 | 44 | // termcolor doesn't handle automatic terminal detection 45 | // https://docs.rs/termcolor/latest/termcolor/#detecting-presence-of-a-terminal 46 | let color = match args.color { 47 | Some(ColorArg::Always) => ColorChoice::Always, 48 | Some(ColorArg::Auto) | None => { 49 | if std::io::stderr().is_terminal() { 50 | ColorChoice::Auto 51 | } else { 52 | ColorChoice::Never 53 | } 54 | } 55 | Some(ColorArg::Never) => ColorChoice::Never, 56 | }; 57 | 58 | // If no machine names are given, output a single CIL file containing all of the policies, 59 | // with the default out file name (out.cil) if an out file name isn't specified. 60 | // Else, if the machine name given is "all", build all of the machines. 61 | // This assumes that "all" is a reserved keyword, so a machine cannot be declared with the name "all". 62 | // Otherwise, output an individual CIL files for each of the machine names given. 63 | // In both of the previous two cases, the name of each output CIL file is the name of the machine + .cil. 64 | let result = if args.machine_names.is_empty() { 65 | let res = compile_combined(policies.iter().map(|s| s as &str).collect()); 66 | match res { 67 | Err(e) => Err(e), 68 | Ok(policy) => { 69 | let mut hm = HashMap::new(); 70 | let mut out_filename = args.out_filename; 71 | out_filename.truncate(out_filename.len() - 4); 72 | hm.insert(out_filename, policy); 73 | Ok(hm) 74 | } 75 | } 76 | } else if args.machine_names.contains(&"all".to_string()) { 77 | compile_machine_policies_all(policies.iter().map(|s| s as &str).collect()) 78 | } else { 79 | compile_machine_policies( 80 | policies.iter().map(|s| s as &str).collect(), 81 | args.machine_names, 82 | ) 83 | }; 84 | match result { 85 | Err(error_list) => print_error(error_list, color), 86 | Ok(machine_hashmap) => { 87 | for (machine_name, (machine_cil, warnings)) in machine_hashmap.iter() { 88 | let out_filename = machine_name.to_owned() + ".cil"; 89 | let mut out_file = File::create(&out_filename)?; 90 | out_file.write_all(machine_cil.as_bytes())?; 91 | if args.package { 92 | build_package(machine_name, &out_filename, "32")?; 93 | } 94 | warnings.print_warnings(color); 95 | } 96 | Ok(()) 97 | } 98 | } 99 | } 100 | 101 | fn print_error(error_list: CascadeErrors, color: ColorChoice) -> std::io::Result<()> { 102 | for e in error_list { 103 | if let ErrorItem::Parse(p) = e { 104 | p.print_diagnostic(color); 105 | } else if let ErrorItem::Compile(c) = e { 106 | c.print_diagnostic(color); 107 | } else { 108 | eprintln!("{e}"); 109 | } 110 | } 111 | Err(Error::new(ErrorKind::InvalidData, "Invalid policy")) 112 | } 113 | 114 | // Create a list of policy files 115 | fn get_policy_files(filenames: Vec) -> Result, Error> { 116 | let mut policy_files = Vec::new(); 117 | for file in filenames { 118 | for entry in WalkDir::new(file) { 119 | let entry = entry?; 120 | if entry.file_type().is_file() && entry.path().extension().unwrap_or_default() == "cas" 121 | { 122 | let filename = entry.path().display().to_string(); 123 | policy_files.push(filename); 124 | } 125 | } 126 | } 127 | Ok(policy_files) 128 | } 129 | -------------------------------------------------------------------------------- /src/bin/casc/package.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | ///Make a full set of policy files, as stored in /etc/selinux 3 | use std::fs; 4 | use std::io::{Error, ErrorKind, Write}; 5 | use std::path::Path; 6 | use std::process::Command; 7 | 8 | use flate2::write::GzEncoder; 9 | use flate2::Compression; 10 | use tar::{Builder, Header}; 11 | 12 | use selinux_cascade::{generate_dbus_contexts, generate_seusers}; 13 | 14 | const FC_NAME: &str = "file_contexts"; 15 | 16 | /// Assumes system_name.cil has already been created by prior functions 17 | pub fn build_package( 18 | system_name: &str, 19 | cil_path: &str, 20 | policy_binary_version: &str, 21 | ) -> std::io::Result<()> { 22 | let policy_name = ["policy.", policy_binary_version].concat(); 23 | let tar_gz = fs::File::create("selinux_policy.tar.gz")?; 24 | let enc = GzEncoder::new(tar_gz, Compression::default()); 25 | let mut tar = tar::Builder::new(enc); 26 | 27 | let output = Command::new("secilc") 28 | .arg(["--policyvers=", policy_binary_version].concat()) 29 | .arg(cil_path) 30 | .output()?; 31 | if !output.status.success() { 32 | if let Ok(stderr) = std::str::from_utf8(&output.stderr) { 33 | eprintln!("{stderr}"); 34 | } 35 | return Err(Error::new( 36 | ErrorKind::InvalidData, 37 | "Compliation of generated CIL failed with message. This is a Cascade bug", 38 | )); 39 | } 40 | add_file_to_tar( 41 | &mut tar, 42 | system_name, 43 | &["policy/", &policy_name].concat(), 44 | &policy_name, 45 | )?; 46 | add_file_to_tar( 47 | &mut tar, 48 | system_name, 49 | "contexts/files/file_contexts", 50 | FC_NAME, 51 | )?; 52 | let dbus_contexts = match generate_dbus_contexts() { 53 | Ok(contexts) => contexts, 54 | Err(e) => { 55 | eprintln!("Failed generating dbus_contexts file: {e}"); 56 | return Err(Error::new( 57 | ErrorKind::InvalidData, 58 | "Generation of dbus_contexts failed. This is a Cascade bug", 59 | )); 60 | } 61 | }; 62 | add_file_to_tar_from_string( 63 | &mut tar, 64 | system_name, 65 | "contexts/dbus_contexts", 66 | &dbus_contexts, 67 | )?; 68 | add_file_to_tar_from_string(&mut tar, system_name, "seusers", &generate_seusers())?; 69 | tar.finish() 70 | } 71 | 72 | fn add_file_to_tar( 73 | tar: &mut Builder, 74 | system_name: &str, 75 | target_path: &str, 76 | file_path: &str, 77 | ) -> std::io::Result<()> 78 | where 79 | W: Write, 80 | { 81 | let mut fd = fs::File::open(file_path)?; 82 | let out_path = Path::new(system_name).join(target_path); 83 | tar.append_file(out_path, &mut fd) 84 | } 85 | 86 | fn add_file_to_tar_from_string( 87 | tar: &mut Builder, 88 | system_name: &str, 89 | target_path: &str, 90 | file_contents: &str, 91 | ) -> std::io::Result<()> 92 | where 93 | W: Write, 94 | { 95 | let mut header = Header::new_gnu(); 96 | header.set_size(file_contents.len().try_into().unwrap()); //TODO: handle error 97 | header.set_mode(0o644); 98 | header.set_cksum(); 99 | let out_path = Path::new(system_name).join(target_path); 100 | tar.append_data(&mut header, out_path, file_contents.as_bytes()) 101 | } 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | use super::*; 106 | use flate2::read::GzDecoder; 107 | use tar::Archive; 108 | 109 | #[test] 110 | fn test_package() { 111 | for version in ["30", "31", "32"] { 112 | build_package("foo", "data/expected_cil/simple.cil", version).unwrap(); 113 | if !Path::new("package_test").exists() { 114 | fs::create_dir("package_test").unwrap(); 115 | } 116 | let tar_gz = fs::File::open("selinux_policy.tar.gz").unwrap(); 117 | let tar = GzDecoder::new(tar_gz); 118 | let mut archive = Archive::new(tar); 119 | archive.unpack("package_test").unwrap(); 120 | 121 | for file in [ 122 | &["policy/policy.", version].concat(), 123 | "contexts/files/file_contexts", 124 | "contexts/dbus_contexts", 125 | ] { 126 | let filename = &["package_test/foo/", file].concat(); 127 | let metadata = fs::metadata(filename).unwrap(); 128 | assert!(metadata.is_file()); 129 | } 130 | 131 | fs::remove_dir_all("package_test").unwrap(); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | pub const ALLOW_FUNCTION_NAME: &str = "allow"; 4 | pub const DONTAUDIT_FUNCTION_NAME: &str = "dontaudit"; 5 | pub const AUDITALLOW_FUNCTION_NAME: &str = "auditallow"; 6 | pub const NEVERALLOW_FUNCTION_NAME: &str = "neverallow"; 7 | pub const FILE_CONTEXT_FUNCTION_NAME: &str = "file_context"; 8 | pub const RESOURCE_TRANS_FUNCTION_NAME: &str = "resource_transition"; 9 | pub const FS_CONTEXT_FUNCTION_NAME: &str = "fs_context"; 10 | pub const DOMTRANS_FUNCTION_NAME: &str = "domain_transition"; 11 | pub const INITIAL_CONTEXT_FUNCTION_NAME: &str = "initial_context"; 12 | pub const SYSTEM_TYPE: &str = "machine_type"; 13 | pub const MONOLITHIC: &str = "monolithic"; 14 | pub const HANDLE_UNKNOWN_PERMS: &str = "handle_unknown_perms"; 15 | pub const PORTCON_FUNCTION_NAME: &str = "portcon"; 16 | 17 | pub const AV_RULES: &[&str] = &[ 18 | ALLOW_FUNCTION_NAME, 19 | DONTAUDIT_FUNCTION_NAME, 20 | AUDITALLOW_FUNCTION_NAME, 21 | NEVERALLOW_FUNCTION_NAME, 22 | ]; 23 | 24 | pub const DOMAIN: &str = "domain"; 25 | pub const RESOURCE: &str = "resource"; 26 | pub const PERM: &str = "perm"; 27 | pub const CLASS: &str = "class"; 28 | pub const MODULE: &str = "module"; 29 | pub const SELF: &str = "self"; 30 | pub const FS_TYPE: &str = "fs_type"; 31 | pub const NUMBER: &str = "number"; 32 | pub const IPADDR: &str = "ipaddr"; 33 | pub const BOOLEAN: &str = "bool"; 34 | 35 | pub const BUILT_IN_TYPES: &[&str] = &[ 36 | DOMAIN, RESOURCE, MODULE, "path", "string", CLASS, PERM, "context", SELF, FS_TYPE, "xattr", 37 | "task", "trans", "genfscon", "*", NUMBER, IPADDR, BOOLEAN, 38 | ]; 39 | 40 | pub const SYSTEM_CONFIG_DEFAULTS: &[(&str, &str)] = 41 | &[(SYSTEM_TYPE, "standard"), (MONOLITHIC, "true")]; 42 | -------------------------------------------------------------------------------- /src/dbus.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Generate selinux dbus busconfig xml information 5 | // https://blog.siphos.be/2014/06/d-bus-and-selinux/ 6 | 7 | use quick_xml::events::{BytesEnd, BytesStart, Event}; 8 | use quick_xml::writer::Writer; 9 | use std::io::Cursor; 10 | use std::str; 11 | 12 | use crate::error::ErrorItem; 13 | 14 | // Generate a string which is the xml for a dbus_contexts file. 15 | // Currently, this only outputs the empty boilerplate. In the long term 16 | // It will support outputting selinux tags understood by dbus 17 | pub fn make_dbus_contexts() -> Result { 18 | let mut writer = Writer::new(Cursor::new(Vec::new())); 19 | writer.write_event(Event::Start(BytesStart::new("busconfig")))?; 20 | writer.write_event(Event::Start(BytesStart::new("selinux")))?; 21 | // associate tags to map dbus services to SELinux labels go here 22 | writer.write_event(Event::End(BytesEnd::new("selinux")))?; 23 | writer.write_event(Event::End(BytesEnd::new("busconfig")))?; 24 | let result = writer.into_inner().into_inner(); // Yes, this is the API 25 | Ok(str::from_utf8(&result)?.to_string()) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | 32 | #[test] 33 | fn test_make_dbus_contexts_empty() { 34 | let dbus_contexts = make_dbus_contexts().unwrap(); 35 | assert_eq!( 36 | dbus_contexts.as_str(), 37 | "" 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | 4 | // If this is named "Backtrace", thiserror assumes its the std::backtrace 5 | // which is only available in nightly, which causes errors on stable. 6 | // https://github.com/dtolnay/thiserror/issues/130 7 | // If Cascade moves to nightly or std::backtrace comes to stable, then 8 | // thiserror can handle backtraces for us with minimal effort 9 | use backtrace::Backtrace as BacktraceCrate; 10 | use codespan_reporting::diagnostic::{Diagnostic, Label}; 11 | use codespan_reporting::files::{SimpleFile, SimpleFiles}; 12 | use codespan_reporting::term; 13 | use lalrpop_util::lexer::Token; 14 | use lalrpop_util::ParseError as LalrpopParseError; 15 | use std::collections::VecDeque; 16 | use std::fmt; 17 | use std::io; 18 | use std::ops::Range; 19 | use termcolor::{ColorChoice, StandardStream}; 20 | use thiserror::Error; 21 | 22 | #[derive(Error, Clone, Debug)] 23 | #[error("{diagnostic}")] 24 | pub struct CompileError { 25 | pub diagnostic: Diag, 26 | pub files: SimpleFiles, 27 | } 28 | 29 | #[derive(Clone, Debug)] 30 | pub struct Diag { 31 | pub inner: Diagnostic, 32 | } 33 | 34 | impl From> for Diag { 35 | fn from(d: Diagnostic) -> Self { 36 | Self { inner: d } 37 | } 38 | } 39 | 40 | impl fmt::Display for Diag { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | //write!(f, "{}:{} {}", self.filename, self.lineno, self.msg) 43 | write!(f, "{}", self.inner.message) 44 | } 45 | } 46 | 47 | impl CompileError { 48 | pub fn new( 49 | msg: &str, 50 | file: &SimpleFile, 51 | range: Range, 52 | help: &str, 53 | ) -> Self { 54 | let mut files = SimpleFiles::new(); 55 | let file_id = files.add(file.name().clone(), file.source().clone()); 56 | 57 | let diagnostic = Diagnostic::error() 58 | .with_message(msg) 59 | .with_labels(vec![Label::primary(file_id, range).with_message(help)]); 60 | 61 | CompileError { 62 | diagnostic: diagnostic.into(), 63 | files, 64 | } 65 | } 66 | pub fn print_diagnostic(&self, color: ColorChoice) { 67 | let writer = StandardStream::stderr(color); 68 | let config = term::Config::default(); 69 | // Ignores print errors. 70 | let _ = term::emit( 71 | &mut writer.lock(), 72 | &config, 73 | &self.files, 74 | &self.diagnostic.inner, 75 | ); 76 | } 77 | 78 | pub fn add_additional_message( 79 | mut self, 80 | file: &SimpleFile, 81 | range: Range, 82 | help: &str, 83 | ) -> Self { 84 | let file_id = self.files.add(file.name().clone(), file.source().clone()); 85 | 86 | self.diagnostic.inner = self 87 | .diagnostic 88 | .inner 89 | .with_labels(vec![Label::primary(file_id, range).with_message(help)]); 90 | self 91 | } 92 | } 93 | 94 | pub fn add_or_create_compile_error( 95 | error: Option, 96 | msg: &str, 97 | file: &SimpleFile, 98 | range: Range, 99 | help: &str, 100 | ) -> CompileError { 101 | if let Some(unwrapped_error) = error { 102 | unwrapped_error.add_additional_message(file, range, help) 103 | } else { 104 | CompileError::new(msg, file, range, help) 105 | } 106 | } 107 | 108 | #[derive(Error, Clone, Debug)] 109 | #[error("{backtrace:?}")] 110 | pub struct InternalError { 111 | backtrace: BacktraceCrate, 112 | } 113 | 114 | impl InternalError { 115 | pub fn new() -> Self { 116 | InternalError { 117 | backtrace: BacktraceCrate::new(), 118 | } 119 | } 120 | } 121 | 122 | #[derive(Error, Clone, Debug)] 123 | #[error("{diagnostic}")] 124 | pub struct ParseError { 125 | pub diagnostic: Diag<()>, 126 | pub file: SimpleFile, 127 | } 128 | 129 | #[derive(Clone, Debug)] 130 | pub struct ParseErrorMsg { 131 | issue: String, 132 | range: Option>, 133 | help: String, 134 | } 135 | 136 | impl ParseErrorMsg { 137 | pub fn new(issue: String, range: Option>, help: String) -> Self { 138 | ParseErrorMsg { issue, range, help } 139 | } 140 | } 141 | 142 | impl From, ParseErrorMsg>> for ParseErrorMsg { 143 | fn from(error: LalrpopParseError, ParseErrorMsg>) -> Self { 144 | match error { 145 | LalrpopParseError::InvalidToken { location } => ParseErrorMsg { 146 | issue: "Unknown character".into(), 147 | range: Some(location..location), 148 | help: String::new(), 149 | }, 150 | LalrpopParseError::UnrecognizedEof { location, expected } => ParseErrorMsg { 151 | issue: "Unexpected end of file".into(), 152 | range: Some(location..location), 153 | help: format!("Expected {}", expected.join(" or ")), 154 | }, 155 | LalrpopParseError::UnrecognizedToken { 156 | token: (l, t, r), 157 | expected, 158 | } => ParseErrorMsg { 159 | issue: if r - l == 1 { 160 | format!("Unexpected character \"{}\"", t.1) 161 | } else { 162 | format!("Unexpected word \"{}\"", t.1) 163 | }, 164 | range: Some(l..r), 165 | help: format!("Expected {}", expected.join(" or ")), 166 | }, 167 | LalrpopParseError::ExtraToken { token: (l, t, r) } => ParseErrorMsg { 168 | issue: if r - l == 1 { 169 | format!("Unintended character \"{}\"", t.1) 170 | } else { 171 | format!("Unintended word \"{}\"", t.1) 172 | }, 173 | range: Some(l..r), 174 | help: String::new(), 175 | }, 176 | LalrpopParseError::User { error } => error, 177 | } 178 | } 179 | } 180 | 181 | impl ParseError { 182 | pub fn new( 183 | error: LalrpopParseError, ParseErrorMsg>, 184 | file_name: String, 185 | policy: String, 186 | ) -> Self { 187 | let msg: ParseErrorMsg = error.into(); 188 | let diagnostic = Diagnostic::error().with_message(msg.issue); 189 | ParseError { 190 | file: SimpleFile::new(file_name, policy), 191 | diagnostic: match msg.range { 192 | None => diagnostic, 193 | Some(range) => { 194 | diagnostic.with_labels(vec![Label::primary((), range).with_message(msg.help)]) 195 | } 196 | } 197 | .into(), 198 | } 199 | } 200 | 201 | pub fn print_diagnostic(&self, color: ColorChoice) { 202 | let writer = StandardStream::stderr(color); 203 | let config = term::Config::default(); 204 | // Ignores print errors. 205 | let _ = term::emit( 206 | &mut writer.lock(), 207 | &config, 208 | &self.file, 209 | &self.diagnostic.inner, 210 | ); 211 | } 212 | } 213 | 214 | #[derive(Error, Clone, Debug)] 215 | #[error("{diagnostic}")] 216 | pub struct InvalidMachineError { 217 | pub diagnostic: Diag, 218 | } 219 | 220 | impl InvalidMachineError { 221 | pub fn new(msg: &str) -> Self { 222 | let diagnostic = Diagnostic::error().with_message(msg); 223 | InvalidMachineError { 224 | diagnostic: diagnostic.into(), 225 | } 226 | } 227 | } 228 | 229 | #[derive(Error, Debug)] 230 | pub enum ErrorItem { 231 | #[error("Compilation error: {0}")] 232 | Compile(#[from] CompileError), 233 | #[error("Internal error: {0}")] 234 | Internal(#[from] InternalError), 235 | #[error("Parsing error: {0}")] 236 | Parse(#[from] ParseError), 237 | // TODO: Replace IO() with semantic errors wrapping io::Error. 238 | #[error("I/O error: {0}")] 239 | IO(#[from] io::Error), 240 | #[error("Invalid machine error: {0}")] 241 | InvalidMachine(#[from] InvalidMachineError), 242 | } 243 | 244 | impl ErrorItem { 245 | pub fn make_compile_or_internal_error( 246 | msg: &str, 247 | file: Option<&SimpleFile>, 248 | range: Option>, 249 | help: &str, 250 | ) -> Self { 251 | match (file, range) { 252 | (Some(f), Some(r)) => ErrorItem::Compile(CompileError::new(msg, f, r, help)), 253 | (_, _) => ErrorItem::Internal(InternalError::new()), 254 | } 255 | } 256 | 257 | // If it's a compile error, add an additional message, otherwise just return the error 258 | pub fn maybe_add_additional_message( 259 | self, 260 | file: Option<&SimpleFile>, 261 | range: Option>, 262 | msg: &str, 263 | ) -> Self { 264 | if let ErrorItem::Compile(error) = self { 265 | if let (Some(file), Some(range)) = (file, range) { 266 | ErrorItem::Compile(error.add_additional_message(file, range, msg)) 267 | } else { 268 | ErrorItem::Internal(InternalError::new()) 269 | } 270 | } else { 271 | self 272 | } 273 | } 274 | } 275 | 276 | impl From for Vec { 277 | fn from(error: ErrorItem) -> Self { 278 | vec![error] 279 | } 280 | } 281 | 282 | // In our case, quick_xml errors are typically code errors on our end. 283 | // If a quick_xml error could be caused by bad user input, then we need to manually create a 284 | // CompileError at the call site. Otherwise, we can just use the below From trait to get an 285 | // Internal Error 286 | impl From for ErrorItem { 287 | fn from(_: quick_xml::Error) -> Self { 288 | // TODO: It would be nice to be able to augment the Internal Error with info about the 289 | // quick_xml error 290 | ErrorItem::Internal(InternalError::new()) 291 | } 292 | } 293 | 294 | impl From for ErrorItem { 295 | fn from(_: std::str::Utf8Error) -> Self { 296 | ErrorItem::Internal(InternalError::new()) 297 | } 298 | } 299 | 300 | #[derive(Error, Debug)] 301 | pub struct CascadeErrors { 302 | errors: VecDeque, 303 | } 304 | 305 | impl CascadeErrors { 306 | pub fn new() -> Self { 307 | CascadeErrors { 308 | errors: VecDeque::new(), 309 | } 310 | } 311 | 312 | pub fn add_error(&mut self, error: T) 313 | where 314 | T: Into, 315 | { 316 | self.errors.push_back(error.into()); 317 | } 318 | 319 | pub fn is_empty(&self) -> bool { 320 | self.errors.is_empty() 321 | } 322 | 323 | pub fn append(&mut self, mut other: CascadeErrors) { 324 | self.errors.append(&mut other.errors); 325 | } 326 | 327 | pub fn into_result_with(self, ok_with: F) -> Result 328 | where 329 | F: FnOnce() -> T, 330 | { 331 | if self.is_empty() { 332 | Ok(ok_with()) 333 | } else { 334 | Err(self) 335 | } 336 | } 337 | 338 | pub fn into_result(self, ok: T) -> Result { 339 | self.into_result_with(|| ok) 340 | } 341 | 342 | /// Enables to easily stop a workflow after a failed major step. This is 343 | /// useful to avoid accumulating more errors that may be hard to understand 344 | /// because of unsatisfied prerequisite. 345 | /// 346 | /// For a multi-step workflow, it works as follow: 347 | /// 1. creates an accumulator with `let mut errors = CascadeErrors::new();` 348 | /// 2. within a major step accumulate errors with `errors.add_error(e);` 349 | /// 3. between major steps check for any errors with `errors = 350 | /// errors.into_result_self()?;` which returns `Err(self)` if there are 351 | /// any. If there aren't, just keep the empty list and proceed. 352 | pub fn into_result_self(self) -> Result { 353 | if self.is_empty() { 354 | Ok(self) 355 | } else { 356 | Err(self) 357 | } 358 | } 359 | 360 | pub fn error_count(&self) -> usize { 361 | self.errors.len() 362 | } 363 | } 364 | 365 | impl From for CascadeErrors { 366 | fn from(error: ErrorItem) -> Self { 367 | CascadeErrors { 368 | errors: VecDeque::from([error]), 369 | } 370 | } 371 | } 372 | 373 | impl From for CascadeErrors { 374 | fn from(error: CompileError) -> Self { 375 | CascadeErrors::from(ErrorItem::from(error)) 376 | } 377 | } 378 | 379 | impl From for CascadeErrors { 380 | fn from(error: InternalError) -> Self { 381 | CascadeErrors::from(ErrorItem::from(error)) 382 | } 383 | } 384 | 385 | impl From for CascadeErrors { 386 | fn from(error: InvalidMachineError) -> Self { 387 | CascadeErrors::from(ErrorItem::from(error)) 388 | } 389 | } 390 | 391 | impl Iterator for CascadeErrors { 392 | type Item = ErrorItem; 393 | fn next(&mut self) -> Option { 394 | self.errors.pop_front() 395 | } 396 | } 397 | 398 | impl fmt::Display for CascadeErrors { 399 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 400 | let num_errors = self.errors.len(); 401 | let s = match num_errors { 402 | 0 => return writeln!(f, "no error"), 403 | 1 => "", 404 | _ => "s", 405 | }; 406 | writeln!(f, "{num_errors} error{s}:")?; 407 | for (i, e) in self.errors.iter().enumerate() { 408 | writeln!(f, "{}: {:#?}", i + 1, e)? 409 | } 410 | Ok(()) 411 | } 412 | } 413 | 414 | #[cfg(test)] 415 | mod tests { 416 | use super::*; 417 | 418 | #[test] 419 | fn multi_file_errors() { 420 | let file1 = SimpleFile::new("File1.cas".to_string(), "Contents of file 1".to_string()); 421 | let file2 = SimpleFile::new("File2.cas".to_string(), "Contents of file 2".to_string()); 422 | 423 | let mut error = CompileError::new( 424 | "This message points at multiple files", 425 | &file1, 426 | 9..11, 427 | "This is the word 'of' in file 1", 428 | ); 429 | 430 | error = error.add_additional_message(&file2, 12..16, "This is the word file in file 2"); 431 | 432 | let labels = error.diagnostic.inner.labels; 433 | assert_eq!(labels.len(), 2); 434 | } 435 | 436 | #[test] 437 | fn error_order() { 438 | let file = SimpleFile::new("name.cas".to_string(), "contents".to_string()); 439 | let mut errors = CascadeErrors::new(); 440 | errors.add_error(InternalError::new()); 441 | errors.add_error(CompileError::new("Some error", &file, 0..1, "help message")); 442 | 443 | assert!(matches!(errors.next(), Some(ErrorItem::Internal(_)))); 444 | assert!(matches!(errors.next(), Some(ErrorItem::Compile(_)))); 445 | assert!(errors.next().is_none()); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | #![allow(clippy::manual_flatten)] 4 | #![allow(clippy::new_without_default)] 5 | #[macro_use] 6 | extern crate lalrpop_util; 7 | 8 | extern crate thiserror; 9 | 10 | mod alias_map; 11 | mod annotations; 12 | mod ast; 13 | mod compile; 14 | mod constants; 15 | mod context; 16 | mod dbus; 17 | pub mod error; 18 | mod functions; 19 | mod internal_rep; 20 | mod machine; 21 | mod obj_class; 22 | mod sexp_internal; 23 | mod util; 24 | pub mod warning; 25 | 26 | #[cfg(test)] 27 | mod test; 28 | 29 | use std::collections::{BTreeMap, BTreeSet, HashMap}; 30 | 31 | use crate::annotations::InsertExtendTiming; 32 | use crate::ast::{Argument, CascadeString, Declaration, Expression, Policy, PolicyFile}; 33 | use crate::context::{BlockType, Context}; 34 | use crate::error::{CascadeErrors, InternalError, InvalidMachineError, ParseErrorMsg}; 35 | use crate::functions::{FunctionClass, FunctionMap}; 36 | use crate::machine::{MachineMap, ModuleMap, ValidatedMachine, ValidatedModule}; 37 | use crate::util::append_set_map; 38 | pub use crate::warning::Warnings; 39 | 40 | use codespan_reporting::files::SimpleFile; 41 | use lalrpop_util::ParseError as LalrpopParseError; 42 | 43 | #[cfg(test)] 44 | use error::ErrorItem; 45 | 46 | lalrpop_mod!(#[allow(clippy::all)] pub parser); 47 | 48 | /// Compile all machines into a single policy 49 | /// 50 | /// The list of input files should contain filenames of files containing policy to be 51 | /// compiled. 52 | /// Returns a Result containing either a string of CIL policy which is the compiled result or a 53 | /// list of errors. 54 | /// In order to convert the compiled CIL policy into a usable policy, you must use secilc. 55 | pub fn compile_combined( 56 | input_files: Vec<&str>, 57 | ) -> Result<(String, Warnings), error::CascadeErrors> { 58 | let errors = CascadeErrors::new(); 59 | let policies = get_policies(input_files)?; 60 | let mut res = compile_machine_policies_internal(policies, vec!["out".to_string()], true)?; 61 | let ret = match res.remove("out") { 62 | Some(s) => s, 63 | None => return Err(CascadeErrors::from(InternalError::new())), 64 | }; 65 | errors.into_result(ret) 66 | } 67 | 68 | /// Compile a complete machine policy 69 | /// 70 | /// The list of input files should contain filenames of files containing policy to be 71 | /// compiled. 72 | /// The list of machine names are the names of the machines to build. 73 | /// Returns a Result containing either a string of CIL policy which is the compiled result or a 74 | /// list of errors. 75 | /// In order to convert the compiled CIL policy into a usable policy, you must use secilc. 76 | pub fn compile_machine_policies( 77 | input_files: Vec<&str>, 78 | machine_names: Vec, 79 | ) -> Result, error::CascadeErrors> { 80 | let policies = get_policies(input_files)?; 81 | compile_machine_policies_internal(policies, machine_names, false) 82 | } 83 | 84 | /// Compile all of the machine policies 85 | /// 86 | /// The list of input files should contain filenames of files containing policy to be 87 | /// compiled. 88 | /// Returns a Result containing either a string of CIL policy which is the compiled result or a 89 | /// list of errors. 90 | /// In order to convert the compiled CIL policy into a usable policy, you must use secilc. 91 | pub fn compile_machine_policies_all( 92 | input_files: Vec<&str>, 93 | ) -> Result, error::CascadeErrors> { 94 | let mut machine_names = Vec::new(); 95 | let policies = get_policies(input_files)?; 96 | for p in &policies { 97 | for e in &p.policy.exprs { 98 | if let Expression::Decl(Declaration::Machine(s)) = e { 99 | machine_names.push(s.name.to_string()); 100 | } 101 | } 102 | } 103 | compile_machine_policies_internal(policies, machine_names, false) 104 | } 105 | 106 | /// Generate a dbus_contexts file 107 | /// 108 | /// In the long term, this needs to take information about the policy to use in the generation 109 | /// For now, all we generate is an xml template 110 | pub fn generate_dbus_contexts() -> Result { 111 | Ok(dbus::make_dbus_contexts()?) 112 | } 113 | 114 | /// Generate an seusers file 115 | pub fn generate_seusers() -> String { 116 | "__default__:system_u".to_string() 117 | } 118 | 119 | fn compile_machine_policies_internal( 120 | mut policies: Vec, 121 | machine_names: Vec, 122 | create_default_machine: bool, 123 | ) -> Result, error::CascadeErrors> { 124 | let mut errors = CascadeErrors::new(); 125 | // This will need to be mutable as we add more warnings 126 | #[allow(unused_mut)] 127 | let mut warnings = Warnings::new(); 128 | 129 | // Generic initialization 130 | let classlist = obj_class::make_classlist(); 131 | let mut type_map = compile::get_built_in_types_map()?; 132 | let mut module_map = ModuleMap::new(); 133 | let mut machine_map = MachineMap::new(); 134 | let mut extend_annotations = BTreeMap::new(); 135 | 136 | { 137 | // Collect all type declarations 138 | for p in &policies { 139 | match compile::extend_type_map(p, &mut type_map) { 140 | Ok(anns) => append_set_map(&mut extend_annotations, &mut anns.inner(&mut warnings)), 141 | Err(e) => { 142 | errors.append(e); 143 | continue; 144 | } 145 | } 146 | } 147 | 148 | compile::insert_extend_annotations( 149 | &mut type_map, 150 | &extend_annotations, 151 | InsertExtendTiming::Early, 152 | ); 153 | 154 | // Stops if something went wrong for this major step. 155 | errors = errors.into_result_self()?; 156 | } 157 | 158 | // Generate type aliases 159 | let (t_aliases, alias_files) = compile::collect_aliases(type_map.iter())?; 160 | type_map.validate_aliases(&t_aliases, &alias_files)?; 161 | type_map.set_aliases(t_aliases); 162 | 163 | for p in &policies { 164 | match compile::verify_extends(p, &type_map) { 165 | Ok(()) => (), 166 | Err(e) => errors.append(e), 167 | } 168 | } 169 | 170 | errors = errors.into_result_self()?; 171 | 172 | // Applies annotations 173 | { 174 | let mut tmp_func_map = FunctionMap::new(); 175 | 176 | // Collect all function declarations 177 | for p in &policies { 178 | let mut m = match compile::build_func_map( 179 | &p.policy.exprs, 180 | &type_map, 181 | &classlist, 182 | FunctionClass::Global, 183 | &p.file, 184 | ) { 185 | Ok(m) => m, 186 | Err(e) => { 187 | errors.append(e); 188 | continue; 189 | } 190 | }; 191 | tmp_func_map.append(&mut m); 192 | } 193 | 194 | // TODO: Validate original functions before adding synthetic ones to avoid confusing errors for users. 195 | match compile::apply_associate_annotations(&type_map, &extend_annotations) { 196 | Ok(exprs) => { 197 | let pf = PolicyFile::new( 198 | Policy::new(exprs), 199 | SimpleFile::new(String::new(), String::new()), 200 | ); 201 | match compile::extend_type_map(&pf, &mut type_map) { 202 | Ok(anns) => { 203 | append_set_map(&mut extend_annotations, &mut anns.inner(&mut warnings)); 204 | policies.push(pf); 205 | } 206 | Err(e) => errors.append(e), 207 | } 208 | } 209 | Err(e) => errors.append(e), 210 | } 211 | compile::insert_extend_annotations( 212 | &mut type_map, 213 | &extend_annotations, 214 | InsertExtendTiming::Late, 215 | ); 216 | } 217 | // Stops if something went wrong for this major step. 218 | errors = errors.into_result_self()?; 219 | 220 | // It would be really nice to do this earlier, but we can't maintain immutable references into 221 | // the type_map across the mutable reference in extend_type_map(). I *think* it's okay to do 222 | // it this late, but if we end up needing the global context in build_func_map() or 223 | // extend_type_map(), we'll need to decouple the type_map references 224 | let mut contexts = Vec::new(); 225 | for p in &policies { 226 | match compile::get_global_bindings(p, &type_map, &classlist, &p.file) { 227 | Ok(c) => contexts.push(c), 228 | Err(e) => { 229 | errors.append(e); 230 | continue; 231 | } 232 | } 233 | } 234 | 235 | let mut global_context = Context::new(BlockType::Global, None, None); 236 | for mut c in contexts { 237 | global_context.drain_symbols(&mut c); 238 | } 239 | 240 | errors = errors.into_result_self()?; 241 | 242 | // Validate modules 243 | compile::validate_modules(&policies, &type_map, &mut module_map)?; 244 | 245 | // Generate module aliases 246 | let (m_aliases, alias_files) = compile::collect_aliases(module_map.iter())?; 247 | module_map.validate_aliases(&m_aliases, &alias_files)?; 248 | module_map.set_aliases(m_aliases); 249 | 250 | // Validate machines 251 | compile::validate_machines(&policies, &module_map, &mut machine_map)?; 252 | 253 | // Create a default module and default machine 254 | // Insert the default module into the default machine and insert the machine into the machine map 255 | let mut default_module: ValidatedModule; 256 | let arg; 257 | if create_default_machine { 258 | default_module = match ValidatedModule::new( 259 | CascadeString::from("module"), 260 | BTreeSet::new(), 261 | BTreeSet::new(), 262 | None, 263 | None, 264 | ) { 265 | Ok(m) => m, 266 | Err(_) => { 267 | return Err(CascadeErrors::from(InternalError::new())); 268 | } 269 | }; 270 | arg = Argument::Var(CascadeString::from("allow")); 271 | for type_info in type_map.values() { 272 | default_module.types.insert(type_info); 273 | } 274 | let mut configs = BTreeMap::new(); 275 | configs.insert(constants::HANDLE_UNKNOWN_PERMS.to_string(), &arg); 276 | let mut default_machine = ValidatedMachine::new( 277 | CascadeString::from(machine_names.first().unwrap().clone()), 278 | BTreeSet::new(), 279 | configs, 280 | None, 281 | ); 282 | default_machine.modules.insert(&default_module); 283 | machine_map.insert(default_machine.name.to_string(), default_machine)?; 284 | } 285 | 286 | let mut machine_hashmap = HashMap::new(); 287 | for machine_name in machine_names { 288 | let mut machine_warnings = warnings.clone(); 289 | match machine_map.get(&machine_name) { 290 | Some(machine) => { 291 | let machine_cil_tree = compile::get_reduced_infos( 292 | &policies, 293 | &classlist, 294 | machine, 295 | &type_map, 296 | &module_map, 297 | &global_context, 298 | )? 299 | .inner(&mut machine_warnings); 300 | 301 | let machine_cil = generate_cil(machine_cil_tree); 302 | 303 | machine_hashmap.insert(machine_name, (machine_cil, machine_warnings)); 304 | } 305 | None => errors.append(CascadeErrors::from(InvalidMachineError::new(&format!( 306 | "Machine {} does not exist.\nThe valid machines are {}", 307 | machine_name, 308 | machine_map 309 | .values() 310 | .map(|s| s.name.as_ref()) 311 | .collect::>() 312 | .join(", ") 313 | )))), 314 | } 315 | } 316 | errors.into_result(machine_hashmap) 317 | } 318 | 319 | fn get_policies(input_files: Vec<&str>) -> Result, CascadeErrors> { 320 | let mut errors = CascadeErrors::new(); 321 | let mut policies: Vec = Vec::new(); 322 | let parser = parser::PolicyParser::new(); 323 | for f in input_files { 324 | let policy_str = match std::fs::read_to_string(f) { 325 | Ok(s) => s, 326 | Err(e) => { 327 | errors.add_error(e); 328 | continue; 329 | } 330 | }; 331 | let p = match parse_policy(&parser, &policy_str) { 332 | Ok(p) => p, 333 | Err(evec) => { 334 | for e in evec { 335 | // TODO: avoid String duplication 336 | errors.add_error(error::ParseError::new(e, f.into(), policy_str.clone())); 337 | } 338 | continue; 339 | } 340 | }; 341 | policies.push(PolicyFile::new(*p, SimpleFile::new(f.into(), policy_str))); 342 | } 343 | errors.into_result(policies) 344 | } 345 | 346 | fn parse_policy<'a>( 347 | parser: &parser::PolicyParser, 348 | policy: &'a str, 349 | ) -> Result, Vec, ParseErrorMsg>>> 350 | { 351 | let mut errors = Vec::new(); 352 | let parse_res = parser.parse(&mut errors, policy); 353 | // errors is a vec of ErrorRecovery. ErrorRecovery is a struct wrapping a ParseError 354 | // and a sequence of discarded characters. We don't need those characters, so we just 355 | // remove the wrapping. 356 | let mut parse_errors: Vec> = 357 | errors.iter().map(|e| e.error.clone()).collect(); 358 | match parse_res { 359 | Ok(p) => { 360 | if !errors.is_empty() { 361 | Err(parse_errors) 362 | } else { 363 | Ok(p) 364 | } 365 | } 366 | Err(e) => { 367 | parse_errors.push(e); 368 | Err(parse_errors) 369 | } 370 | } 371 | } 372 | 373 | fn generate_cil(v: Vec) -> String { 374 | v.iter() 375 | .map(sexp_internal::display_cil) 376 | .collect::>() 377 | .join("\n") 378 | } 379 | -------------------------------------------------------------------------------- /src/machine.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | 4 | use std::cmp::Ordering; 5 | use std::collections::{BTreeMap, BTreeSet}; 6 | use std::ops::Range; 7 | 8 | use codespan_reporting::files::SimpleFile; 9 | 10 | use crate::alias_map::{AliasMap, Declared}; 11 | use crate::annotations::{Annotated, AnnotationInfo}; 12 | use crate::ast::{Annotations, Argument, CascadeString, Module}; 13 | use crate::error::{CascadeErrors, ErrorItem}; 14 | use crate::internal_rep::TypeInfo; 15 | 16 | pub type ModuleMap<'a> = AliasMap>; 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct ValidatedModule<'a> { 20 | pub name: CascadeString, 21 | pub annotations: BTreeSet, 22 | pub types: BTreeSet<&'a TypeInfo>, 23 | pub validated_modules: BTreeSet<&'a CascadeString>, 24 | declaration_file: Option>, 25 | } 26 | 27 | impl Declared for ValidatedModule<'_> { 28 | fn get_file(&self) -> Option> { 29 | self.declaration_file.clone() 30 | } 31 | 32 | fn get_name_range(&self) -> Option> { 33 | self.name.get_range() 34 | } 35 | 36 | fn get_generic_name(&self) -> String { 37 | String::from("module") 38 | } 39 | 40 | fn get_secondary_indices(&self) -> Vec { 41 | Vec::new() 42 | } 43 | } 44 | 45 | impl Annotated for &ValidatedModule<'_> { 46 | fn get_annotations(&self) -> std::collections::btree_set::Iter { 47 | self.annotations.iter() 48 | } 49 | } 50 | 51 | impl Eq for ValidatedModule<'_> {} 52 | 53 | impl Ord for ValidatedModule<'_> { 54 | fn cmp(&self, other: &Self) -> Ordering { 55 | self.name.cmp(&other.name) 56 | } 57 | } 58 | 59 | impl PartialOrd for ValidatedModule<'_> { 60 | fn partial_cmp(&self, other: &Self) -> Option { 61 | Some(self.cmp(other)) 62 | } 63 | } 64 | 65 | impl PartialEq for ValidatedModule<'_> { 66 | fn eq(&self, other: &Self) -> bool { 67 | self.name == other.name 68 | } 69 | } 70 | 71 | impl<'a> ValidatedModule<'a> { 72 | pub fn new( 73 | name: CascadeString, 74 | types: BTreeSet<&'a TypeInfo>, 75 | validated_modules: BTreeSet<&'a CascadeString>, 76 | mod_decl: Option<&'a Module>, 77 | declaration_file: Option>, 78 | ) -> Result, CascadeErrors> { 79 | let mut module_annontations = BTreeSet::new(); 80 | if let Some(md) = mod_decl { 81 | if let Some(ref df) = declaration_file { 82 | module_annontations = get_module_annotations(df, &md.annotations)?; 83 | } 84 | } 85 | Ok(ValidatedModule { 86 | name, 87 | annotations: module_annontations, 88 | types, 89 | validated_modules, 90 | declaration_file, 91 | }) 92 | } 93 | } 94 | 95 | fn get_module_annotations( 96 | file: &SimpleFile, 97 | annotations: &Annotations, 98 | ) -> Result, ErrorItem> { 99 | let mut infos = BTreeSet::new(); 100 | for annotation in annotations.annotations.iter() { 101 | match annotation.name.as_ref() { 102 | "alias" => { 103 | for arg in &annotation.arguments { 104 | match arg { 105 | Argument::Var(a) => { 106 | infos.insert(AnnotationInfo::Alias(a.clone())); 107 | } 108 | _ => { 109 | return Err(ErrorItem::make_compile_or_internal_error( 110 | "Invalid alias", 111 | Some(file), 112 | annotation.name.get_range(), 113 | "Alias name must be a symbol", 114 | )); 115 | } 116 | } 117 | } 118 | } 119 | _ => { 120 | return Err(ErrorItem::make_compile_or_internal_error( 121 | "Unknown annotation", 122 | Some(file), 123 | annotation.name.get_range(), 124 | "The only valid annotation for modules is '@alias'", 125 | )); 126 | } 127 | } 128 | } 129 | Ok(infos) 130 | } 131 | 132 | pub type MachineMap<'a> = AliasMap>; 133 | 134 | #[derive(Debug, Clone)] 135 | pub struct ValidatedMachine<'a> { 136 | pub name: CascadeString, 137 | pub modules: BTreeSet<&'a ValidatedModule<'a>>, 138 | pub configurations: BTreeMap, 139 | declaration_file: Option>, 140 | } 141 | 142 | impl Declared for ValidatedMachine<'_> { 143 | fn get_file(&self) -> Option> { 144 | self.declaration_file.clone() 145 | } 146 | 147 | fn get_name_range(&self) -> Option> { 148 | self.name.get_range() 149 | } 150 | 151 | fn get_generic_name(&self) -> String { 152 | String::from("machine") 153 | } 154 | fn get_secondary_indices(&self) -> Vec { 155 | Vec::new() 156 | } 157 | } 158 | 159 | impl<'a> ValidatedMachine<'a> { 160 | pub fn new( 161 | name: CascadeString, 162 | modules: BTreeSet<&'a ValidatedModule<'a>>, 163 | configurations: BTreeMap, 164 | declaration_file: Option>, 165 | ) -> Self { 166 | ValidatedMachine { 167 | name, 168 | modules, 169 | configurations, 170 | declaration_file, 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/parser.lalrpop: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | use crate::ast::{CascadeString, Policy, Declaration, DeclarationModifier, Expression, Statement, TypeDecl, CollectionDecl, FuncDecl, Argument, Annotation, LetBinding, Virtualable, FuncCall, DeclaredArgument, Module, Machine, MachineBody, Port, IpAddr as AstIpAddr, IfBlock, OptionalBlock}; 4 | use lalrpop_util::ErrorRecovery; 5 | use lalrpop_util::ParseError; 6 | use crate::error::ParseErrorMsg; 7 | 8 | grammar<'err>(errors: &'err mut Vec, ParseErrorMsg>>); 9 | 10 | extern { 11 | type Error = ParseErrorMsg; 12 | } 13 | 14 | // http://lalrpop.github.io/lalrpop/tutorial/006_macros.html 15 | Comma: Vec = { 16 | ",")*> => match e { 17 | None => v, 18 | Some(e) => { 19 | v.push(e); 20 | v 21 | } 22 | } 23 | }; 24 | 25 | pub Policy: Box = { 26 | Expr+ => Box::new(Policy::new(<>)), 27 | } 28 | 29 | Annotated: T = { 30 | > => { 31 | t.add_annotation(a); 32 | t 33 | }, 34 | T 35 | } 36 | 37 | pub Expr: Expression = { 38 | Annotated, 39 | // On error, report and fast forward to the next expression 40 | ! => { errors.push(<>); Expression::Error }, 41 | } 42 | 43 | BaseExpr: Expression = { 44 | =>? { 45 | match m { 46 | Some(DeclarationModifier::Virtual(range)) => { 47 | match d.set_virtual(range) { 48 | Ok(()) => (), 49 | Err(e) => return Err(ParseError::User { 50 | error: e}) 51 | } 52 | } 53 | Some(DeclarationModifier::Trait(range)) => { 54 | match d.set_trait(range) { 55 | Ok(()) => (), 56 | Err(e) => return Err(ParseError::User { 57 | error: e}) 58 | } 59 | } 60 | None => () 61 | } 62 | Ok(Expression::Decl(d)) 63 | }, 64 | => Expression::Stmt(<>), 65 | } 66 | 67 | DeclModifier: DeclarationModifier = { 68 | => DeclarationModifier::Virtual(start..end), 69 | => DeclarationModifier::Trait(start..end), 70 | } 71 | 72 | Decl: Declaration = { 73 | TypeDecl => Declaration::Type(<>), 74 | CollectionDecl => Declaration::Collection(<>), 75 | FuncDecl => Declaration::Func(<>), 76 | ModuleDecl => Declaration::Mod(<>), 77 | MachineDecl => Declaration::Machine(<>), 78 | } 79 | 80 | TypeDecl: Box = { 81 | "{" "}" => { 82 | let mut inherits = i.unwrap_or_else(|| Vec::new()); 83 | let mut is_extend = false; 84 | match keyword { 85 | Some(dr) => inherits.push(dr), 86 | None => is_extend = true, 87 | } 88 | v.iter_mut().for_each(|e| e.set_class_name_if_decl(n.clone())); 89 | let mut td = TypeDecl::new(n, inherits, v); 90 | if is_extend { 91 | td.set_extend(); 92 | } 93 | Box::new(td) 94 | }, 95 | "extend" "{" "}" => { 96 | // extending built-ins does not support inheritance 97 | v.iter_mut().for_each(|e| e.set_class_name_if_decl(b.clone())); 98 | let mut td = TypeDecl::new(b, Vec::new(), v); 99 | td.set_extend(); 100 | Box::new(td) 101 | } 102 | } 103 | 104 | BuiltInOrExtend: Option = { 105 | BuiltInType => Some(<>), 106 | "extend" => None, 107 | } 108 | 109 | CollectionDecl: Box = { 110 | "collection" "{" *> "}" => { 111 | v.iter_mut().for_each(|f| f.class_name = Some(n.clone())); 112 | Box::new(CollectionDecl::new(n, v)) 113 | }, 114 | } 115 | 116 | InheritList: Vec = { 117 | "inherits" >, 118 | } 119 | 120 | BuiltInType: CascadeString = { 121 | => CascadeString::new(s.to_string(), start..end), 122 | => CascadeString::new(s.to_string(), start..end), 123 | } 124 | 125 | FuncDecl: Box = { 126 | "fn" "(" > ")" "{" "}" => Box::new(FuncDecl::new(n, a, b)), 127 | } 128 | 129 | FuncDeclArg: DeclaredArgument = { 130 | => DeclaredArgument { param_type: t, is_list_param: false, name: n, default: v }, 131 | "[" "]" => DeclaredArgument { param_type: t, is_list_param: true, name: n, default: v }, 132 | } 133 | 134 | #[inline] 135 | DefaultArg: Argument = { 136 | "=" => <> 137 | } 138 | 139 | Stmt: Statement = { 140 | ";", 141 | => Statement::IfBlock(Box::new(<>)), 142 | => Statement::OptionalBlock(Box::new(<>)), 143 | } 144 | 145 | StmtBody: Statement = { 146 | > ".")?> "(" > ")" => { 147 | let mut call = FuncCall::new_with_casts(c, n, a); 148 | if d.is_some() { 149 | call.set_drop(); 150 | } 151 | Statement::Call(Box::new(call)) 152 | }, 153 | => Statement::LetBinding(Box::new(<>)), 154 | } 155 | 156 | LetBind: LetBinding = { 157 | "let" "=" => LetBinding::new(n, a), 158 | } 159 | 160 | ModuleDecl: Module = { 161 | "module" "{" "}" => Module::new(n).set_fields(x), 162 | } 163 | 164 | ModuleBody: (CascadeString, CascadeString) = { 165 | ";" => (s, n), 166 | ";" => (CascadeString::new(s.to_string(), start..end), n), 167 | } 168 | 169 | MachineDecl: Machine = { 170 | "machine" "{" "}" => Machine::new(n).set_fields(x), 171 | } 172 | 173 | SysBody: MachineBody = { 174 | "module" ";" => MachineBody::Mod(n), 175 | ";" => MachineBody::Config(l), 176 | } 177 | 178 | Ann: Annotation = { 179 | "@" "(" > ")" => Annotation::new(s).set_arguments(a), 180 | "@" => Annotation::new(s), 181 | } 182 | 183 | TypeName: CascadeString = { 184 | Symbol, 185 | "." => CascadeString::new([l.as_ref(), ".", r.as_ref()].concat(), start..end), 186 | "*" => CascadeString::new("*".to_string(), start..end) 187 | } 188 | 189 | pub NameDecl: CascadeString = { 190 | // Naming rules: 191 | // * must start with a letter 192 | // * must not end with an underscore 193 | // * must not contain consecutive underscores 194 | // * can contain letters, digits and underscores 195 | => CascadeString::new(s.to_string(), start..end), 196 | } 197 | 198 | Symbol: CascadeString = { 199 | NameDecl, 200 | BuiltInType 201 | } 202 | 203 | List: Vec = { 204 | "[" "]" 205 | } 206 | 207 | // TODO: Define boolean struct 208 | BooleanExpr: () = { 209 | BoolTerm, 210 | BooleanExpr "&&" BoolTerm, 211 | BooleanExpr "||" BoolTerm, 212 | } 213 | 214 | BoolTerm: () = { 215 | Symbol, 216 | "(" BooleanExpr ")", 217 | "!" BoolTerm, 218 | } 219 | 220 | IfBlock: IfBlock = { 221 | "if" "(" BooleanExpr ")" "{" "}" => { 222 | IfBlock { 223 | keyword_range: start..end, 224 | if_statements: then, 225 | else_statements: match e { 226 | Some(e) => e, 227 | None => Vec::new() 228 | } 229 | }} 230 | } 231 | 232 | #[inline] 233 | ElseBlock: Vec = { 234 | "else" "{" "}" => <> 235 | } 236 | 237 | OptionalBlock: OptionalBlock = { 238 | "optional" "{" "}" => OptionalBlock::new(<>) 239 | } 240 | 241 | #[inline] 242 | Casted: (T, Option) = { 243 | ">")?> => (t, c), 244 | } 245 | 246 | CastArg: (Argument, Option) = { 247 | Casted, 248 | } 249 | 250 | Arg: Argument = { 251 | TypeName => Argument::Var(<>), 252 | "=" => Argument::Named(s, Box::new(a)), 253 | List => Argument::List(<>), 254 | Quoted_String => Argument::Quote(<>), 255 | PortRange => Argument::Port(<>), 256 | IPAddr => Argument::IpAddr(<>), 257 | Context => Argument::Var(<>), 258 | } 259 | 260 | Context: CascadeString = { 261 | // TODO: don't discard the mls range 262 | ":" ":" )?> => { 263 | CascadeString::new([u.as_ref(), r.as_ref(), t.as_ref()].join(":"), start..end) 264 | } 265 | } 266 | 267 | MLS_Range: CascadeString = { 268 | )?> => { 269 | match high { 270 | Some(high) => CascadeString::from(&[&low, &CascadeString::from("-"), &high]), 271 | None => low 272 | } 273 | } 274 | } 275 | 276 | MLS_Level: CascadeString = { 277 | )?> => { 278 | match c { 279 | Some(c) => CascadeString::from(&[&s, &CascadeString::from(":"), &c]), 280 | None => s 281 | } 282 | } 283 | } 284 | 285 | Sensitivity: CascadeString = { 286 | NameDecl, 287 | } 288 | 289 | Categories: CascadeString = { 290 | Category, 291 | "." => CascadeString::from(&[&cs, &CascadeString::from("."), &c]), 292 | } 293 | 294 | Category: CascadeString = { 295 | NameDecl, 296 | } 297 | 298 | Quoted_String: CascadeString = { 299 | => CascadeString::new(s.to_string(), start..end), 300 | } 301 | 302 | PortRange: Port = { 303 | => <>, 304 | "-" => { 305 | low.high_port_num = Some(high.low_port_num); 306 | if let (Some(low_range), Some(high_range)) = (low.get_range(), high.get_range()) { 307 | low.range = Some(low_range.start..high_range.end) 308 | } // else should never occur, because these are explicitly Some() in the definition of Port 309 | low 310 | } 311 | } 312 | 313 | Port: Port = { 314 | => Port::new(p.parse().unwrap(), Some(start..end)) // TODO: report parse error instead of panicking 315 | } 316 | 317 | IPAddr: AstIpAddr = { 318 | IPv4 => AstIpAddr::new(<>.as_ref().parse().unwrap(), <>.get_range()), // TODO 319 | IPv6 => AstIpAddr::new(<>.as_ref().parse().unwrap(), <>.get_range()), // TODO, 320 | } 321 | 322 | IPv4: CascadeString = { 323 | => CascadeString::new(ip.to_string(), start..end), 324 | "localhost" => CascadeString::new("localhost".to_string(), start..end), 325 | } 326 | 327 | IPv6: CascadeString = { 328 | => CascadeString::new(ip.to_string(), start..end), 329 | } 330 | 331 | // lexing precedence 332 | match { 333 | r"[[:space:]]+" => { }, 334 | r"//[^\n\r]*[\n\r]*" => { }, 335 | r"([[:digit:]]{1,3}\.){4}" => IPv4Regex, 336 | "::1" => IPv6Regex, // TODO 337 | r"[[:digit:]]+" => PortRegex, 338 | } else { 339 | _ 340 | } 341 | -------------------------------------------------------------------------------- /src/sexp_internal.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | use sexp::*; 4 | 5 | // the sexp crate doesn't treat foo and "foo" as separate strings, which we need 6 | // The quoting behavior in the sexp crate automatically handles quoting in situations where the 7 | // string contains a quote or a space, so we need to avoid those in order for this to work, but 1. 8 | // We want to avoid those anyways and 2. The default behavior makes actually inserting a quoted 9 | // string that wouldn't be automatically quoted impossible. 10 | // https://github.com/cgaebel/sexp/issues/2 11 | pub fn display_cil(expr: &sexp::Sexp) -> String { 12 | match expr { 13 | Sexp::List(l) => { 14 | format!( 15 | "({})", 16 | l.iter().map(display_cil).collect::>().join(" ") 17 | ) 18 | } 19 | Sexp::Atom(a) => match a { 20 | Atom::S(s) => { 21 | if s.starts_with(';') { 22 | format!("\n{s}\n") 23 | } else { 24 | s.to_string() 25 | } 26 | } 27 | _ => a.to_string(), 28 | }, 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | 36 | #[test] 37 | fn test_display_cil() { 38 | let cil = parse("(foo)").unwrap(); 39 | assert_eq!(display_cil(&cil), cil.to_string()); 40 | 41 | let cil = parse("(foo (bar baz))").unwrap(); 42 | assert_eq!(display_cil(&cil), cil.to_string()); 43 | 44 | let cil = parse("foo").unwrap(); 45 | assert_eq!(display_cil(&cil), cil.to_string()); 46 | 47 | let cil = parse("32").unwrap(); 48 | assert_eq!(display_cil(&cil), cil.to_string()); 49 | 50 | let cil = atom_s("\"/bin\""); 51 | assert_eq!(display_cil(&cil), "\"/bin\"".to_string()); 52 | 53 | let cil = atom_s(";comment"); 54 | assert_eq!(display_cil(&cil), "\n;comment\n".to_string()); 55 | 56 | let cil = list(&[atom_s("a"), atom_s(";b"), atom_s("c"), atom_s("d")]); 57 | assert_eq!(display_cil(&cil), "(a \n;b\n c d)".to_string()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | 4 | use std::collections::{BTreeMap, BTreeSet}; 5 | 6 | // BTreeMap::append overwrites the original key if appending a duplicate 7 | // Something that we do a few times in Cascade is to have a BTreeMap>, and in that 8 | // case, we would like to insert the values from other::V into self::V on key collision 9 | pub fn append_set_map( 10 | orig: &mut BTreeMap>, 11 | other: &mut BTreeMap>, 12 | ) { 13 | while let Some((k, mut v)) = other.pop_first() { 14 | match orig.get_mut(&k) { 15 | Some(val) => val.append(&mut v), 16 | None => { 17 | orig.insert(k, v); 18 | } 19 | } 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use super::*; 26 | 27 | #[test] 28 | fn append_set_map_test() { 29 | let mut map1 = BTreeMap::new(); 30 | map1.insert("foo", BTreeSet::from(["a", "b"])); 31 | map1.insert("bar", BTreeSet::from(["c", "d"])); 32 | let mut map2 = BTreeMap::new(); 33 | map2.insert("foo", BTreeSet::from(["e", "f"])); 34 | map2.insert("baz", BTreeSet::from(["g", "h"])); 35 | 36 | append_set_map(&mut map1, &mut map2); 37 | 38 | assert!(map2.is_empty()); 39 | 40 | let mut expected_result = BTreeMap::new(); 41 | expected_result.insert("foo", BTreeSet::from(["a", "b", "e", "f"])); 42 | expected_result.insert("bar", BTreeSet::from(["c", "d"])); 43 | expected_result.insert("baz", BTreeSet::from(["g", "h"])); 44 | assert_eq!(map1, expected_result); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/warning.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //! Module for Cascade warning support 5 | //! 6 | //! If your function calls a function that adds warnings, first create a 7 | //! Warnings object to store the warnings. Then call inner() on the 8 | //! WithWarnings object returned from the function generating warnings. 9 | //! Store your warnings in your warnings struct and then use 10 | //! WithWarnings::new() to return them up to the next level 11 | //! If you add warnings yourself, call add_warnings() on a WithWarnings 12 | //! object 13 | //! See the warnings_usage() test in this module for an example workflow 14 | 15 | use codespan_reporting::diagnostic::Severity; 16 | use codespan_reporting::files::SimpleFile; 17 | use std::ops::Range; 18 | use termcolor::ColorChoice; 19 | 20 | use crate::error::CompileError; 21 | 22 | #[derive(Clone, Debug)] 23 | pub struct Warning { 24 | inner: CompileError, 25 | } 26 | 27 | impl Warning { 28 | pub fn new( 29 | msg: &str, 30 | file: &SimpleFile, 31 | range: Range, 32 | help: &str, 33 | ) -> Self { 34 | let mut error = CompileError::new(msg, file, range, help); 35 | error.diagnostic.inner.severity = Severity::Warning; 36 | Warning { inner: error } 37 | } 38 | 39 | pub fn print_diagnostic(&self, color: ColorChoice) { 40 | self.inner.print_diagnostic(color) 41 | } 42 | } 43 | 44 | #[derive(Clone, Debug)] 45 | pub struct Warnings { 46 | // Using a BTreeSet here would require CompileError to implement Ord. 47 | inner: Vec, 48 | } 49 | 50 | impl Warnings { 51 | pub fn new() -> Self { 52 | Warnings { inner: Vec::new() } 53 | } 54 | 55 | pub fn append(&mut self, other: &mut Self) { 56 | self.inner.append(&mut other.inner) 57 | } 58 | 59 | pub fn push(&mut self, w: Warning) { 60 | self.inner.push(w) 61 | } 62 | 63 | pub fn is_empty(&self) -> bool { 64 | self.inner.is_empty() 65 | } 66 | 67 | pub fn print_warnings(&self, color: ColorChoice) { 68 | for e in &self.inner { 69 | e.print_diagnostic(color) 70 | } 71 | } 72 | 73 | pub fn count(&self) -> usize { 74 | self.inner.len() 75 | } 76 | } 77 | 78 | /// Wraps a Cascade object with additional information about warnings 79 | pub struct WithWarnings { 80 | inner: T, 81 | warnings: Warnings, 82 | } 83 | 84 | impl WithWarnings { 85 | pub fn new(inner: T, warnings: Warnings) -> Self { 86 | WithWarnings { inner, warnings } 87 | } 88 | 89 | /// Return the inner, extract the warnings to the warnings variable 90 | pub fn inner(mut self, warnings: &mut Warnings) -> T { 91 | warnings.append(&mut self.warnings); 92 | self.inner 93 | } 94 | 95 | pub fn add_warning(&mut self, warning: Warning) { 96 | self.warnings.push(warning); 97 | } 98 | } 99 | 100 | impl From for WithWarnings { 101 | fn from(inner: T) -> Self { 102 | WithWarnings { 103 | inner, 104 | warnings: Warnings::new(), 105 | } 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use super::*; 112 | use crate::error::Diag; 113 | use codespan_reporting::diagnostic::Diagnostic; 114 | use codespan_reporting::files::SimpleFile; 115 | 116 | fn maybe_warn( 117 | warn_string: String, 118 | call_count: i8, 119 | do_warn: bool, 120 | file: &SimpleFile, 121 | ) -> WithWarnings { 122 | let mut ret = WithWarnings::from(warn_string); 123 | if do_warn { 124 | ret.add_warning(Warning::new( 125 | &format!("Some warning {}", call_count), 126 | file, 127 | 2..4, // doesn't matter for the test 128 | "Some substring", 129 | )); 130 | } 131 | ret 132 | } 133 | 134 | #[test] 135 | fn basic_warning_test() { 136 | let warn = Warning::new( 137 | "This is a warning", 138 | &SimpleFile::new("file.cas".to_string(), "File contents".to_string()), 139 | 0..4, 140 | "This is the word file", 141 | ); 142 | 143 | assert!(matches!(warn, 144 | Warning { 145 | inner: CompileError { 146 | diagnostic: Diag { 147 | inner: Diagnostic { 148 | message: msg, 149 | .. 150 | } 151 | }, 152 | .. 153 | } 154 | } if msg.contains("This is a warning"))); 155 | } 156 | 157 | #[test] 158 | fn warnings_usage() { 159 | let mut my_string = "some_string".to_string(); 160 | let mut warnings = Warnings::new(); 161 | let file = SimpleFile::new("file.cas".to_string(), "File contents".to_string()); 162 | 163 | my_string = maybe_warn(my_string, 1, false, &file).inner(&mut warnings); 164 | 165 | assert_eq!(&my_string, "some_string"); 166 | assert_eq!(warnings.inner.len(), 0); 167 | 168 | my_string = maybe_warn(my_string, 2, true, &file).inner(&mut warnings); 169 | 170 | assert_eq!(&my_string, "some_string"); 171 | assert_eq!(warnings.inner.len(), 1); 172 | assert!(matches!(&warnings.inner[0], 173 | Warning { 174 | inner: CompileError { 175 | diagnostic: Diag { 176 | inner: Diagnostic { 177 | message: msg, 178 | .. 179 | } 180 | }, 181 | .. 182 | } 183 | } if msg.contains("Some warning 2"))); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tools/update-expected-cil.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) Microsoft Corporation. 3 | # SPDX-License-Identifier: MIT 4 | 5 | set -u -e -o pipefail 6 | 7 | cd "$(dirname -- "$0")/.." 8 | 9 | cargo build --bin casc 10 | 11 | for f in data/policies/*.cas; do 12 | printf '[ ] %s' "$f" 13 | if ./target/debug/casc "$f" 2>/dev/null; then 14 | mv out.cil "data/expected_cil/$(basename -- "${f%%.cas}").cil" 15 | printf '\r[+]\n' 16 | else 17 | rm out.cil 2>/dev/null || true 18 | printf '\r[-]\n' 19 | fi 20 | done 21 | --------------------------------------------------------------------------------