├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── set-up-rust │ │ └── action.yml ├── dependabot.yml ├── pull_request_template.md ├── scripts │ ├── check-regressions.js │ ├── dedup.js │ ├── package.json │ └── test.js └── workflows │ ├── check-regressions.yml │ ├── docker-build.yml │ ├── ghcr.yml │ ├── integration-tests.yaml │ ├── release.yml │ ├── rust.yaml │ ├── sca.yml │ ├── test-rules.yaml │ ├── verify-schema.yaml │ └── versions-check.yaml ├── .gitignore ├── .gitlab-ci.yml ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── DEVELOPMENT.md ├── Dockerfile ├── FAQ.md ├── LICENSE ├── LICENSE-3rdparty.csv ├── NOTICE ├── README.md ├── RULESETS.md ├── crates ├── bins │ ├── Cargo.toml │ └── src │ │ ├── bin │ │ ├── datadog-export-rulesets.rs │ │ ├── datadog-static-analyzer-git-hook.rs │ │ ├── datadog-static-analyzer-server.rs │ │ ├── datadog-static-analyzer-test-ruleset.rs │ │ ├── datadog-static-analyzer.rs │ │ └── datadog_static_analyzer_server │ │ │ ├── cli.rs │ │ │ ├── endpoints.rs │ │ │ ├── error_codes.rs │ │ │ ├── fairings.rs │ │ │ ├── ide │ │ │ ├── Makefile.toml │ │ │ ├── configuration_file │ │ │ │ ├── comment_preserver │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── models.rs │ │ │ │ │ └── reconciler.rs │ │ │ │ ├── endpoints.rs │ │ │ │ ├── error.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── models.rs │ │ │ │ └── static_analysis_config_file.rs │ │ │ ├── mod.rs │ │ │ └── requests.http │ │ │ ├── mod.rs │ │ │ ├── rule_cache.rs │ │ │ ├── state.rs │ │ │ └── utils.rs │ │ └── lib.rs ├── cli │ ├── Cargo.toml │ ├── resources │ │ └── test │ │ │ ├── gitignore │ │ │ └── test1 │ │ │ └── test_files_by_size │ │ │ ├── versions-empty.json │ │ │ └── versions.json │ └── src │ │ ├── config_file.rs │ │ ├── constants.rs │ │ ├── csv.rs │ │ ├── datadog_utils.rs │ │ ├── file_utils.rs │ │ ├── git_utils.rs │ │ ├── lib.rs │ │ ├── model.rs │ │ ├── model │ │ ├── cli_configuration.rs │ │ └── datadog_api.rs │ │ ├── rule_utils.rs │ │ ├── sarif.rs │ │ ├── sarif │ │ ├── sarif-schema-2.1.0.json │ │ └── sarif_utils.rs │ │ ├── utils.rs │ │ └── violations_table.rs ├── common │ ├── Cargo.toml │ └── src │ │ ├── analysis_options.rs │ │ ├── lib.rs │ │ ├── model.rs │ │ ├── model │ │ ├── diff_aware.rs │ │ └── position.rs │ │ ├── utils.rs │ │ └── utils │ │ └── position_utils.rs ├── secrets │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── model.rs │ │ ├── model │ │ ├── secret_result.rs │ │ └── secret_rule.rs │ │ ├── scanner.rs │ │ └── secret_files.rs ├── static-analysis-kernel │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── analysis.rs │ │ ├── analysis │ │ ├── analyze.rs │ │ ├── ddsa_lib.rs │ │ ├── ddsa_lib │ │ │ ├── bridge.rs │ │ │ ├── bridge │ │ │ │ ├── context.rs │ │ │ │ ├── query_match.rs │ │ │ │ ├── ts_node.rs │ │ │ │ └── violation.rs │ │ │ ├── common.rs │ │ │ ├── context.rs │ │ │ ├── context │ │ │ │ ├── file.rs │ │ │ │ ├── file_go.rs │ │ │ │ ├── file_js.rs │ │ │ │ ├── file_tf.rs │ │ │ │ ├── root.rs │ │ │ │ ├── rule.rs │ │ │ │ └── ts_lang.rs │ │ │ ├── extension.rs │ │ │ ├── js.rs │ │ │ ├── js │ │ │ │ ├── __bootstrap.js │ │ │ │ ├── capture.js │ │ │ │ ├── capture.rs │ │ │ │ ├── context_file.js │ │ │ │ ├── context_file.rs │ │ │ │ ├── context_file_go.js │ │ │ │ ├── context_file_go.rs │ │ │ │ ├── context_file_js.js │ │ │ │ ├── context_file_js.rs │ │ │ │ ├── context_file_tf.js │ │ │ │ ├── context_file_tf.rs │ │ │ │ ├── context_root.js │ │ │ │ ├── context_root.rs │ │ │ │ ├── context_rule.js │ │ │ │ ├── context_rule.rs │ │ │ │ ├── context_ts_lang.js │ │ │ │ ├── context_ts_lang.rs │ │ │ │ ├── ddsa.js │ │ │ │ ├── ddsa.rs │ │ │ │ ├── edit.js │ │ │ │ ├── edit.rs │ │ │ │ ├── fix.js │ │ │ │ ├── fix.rs │ │ │ │ ├── flow.rs │ │ │ │ ├── flow │ │ │ │ │ ├── graph.js │ │ │ │ │ ├── graph.rs │ │ │ │ │ ├── graph_test_utils.rs │ │ │ │ │ ├── java.js │ │ │ │ │ └── java.rs │ │ │ │ ├── globals.d.js │ │ │ │ ├── query_match.js │ │ │ │ ├── query_match.rs │ │ │ │ ├── query_match_compat.js │ │ │ │ ├── query_match_compat.rs │ │ │ │ ├── region.js │ │ │ │ ├── region.rs │ │ │ │ ├── stella_compat.js │ │ │ │ ├── stella_compat.rs │ │ │ │ ├── test_helpers.js │ │ │ │ ├── ts_node.js │ │ │ │ ├── ts_node.rs │ │ │ │ ├── utility.js │ │ │ │ ├── utility.rs │ │ │ │ ├── violation.js │ │ │ │ └── violation.rs │ │ │ ├── ops.rs │ │ │ ├── resource_watchdog.rs │ │ │ ├── runtime.rs │ │ │ ├── test_utils.rs │ │ │ ├── v8_ds.rs │ │ │ └── v8_platform.rs │ │ ├── generated_content.rs │ │ ├── languages.rs │ │ ├── languages │ │ │ ├── csharp.rs │ │ │ ├── csharp │ │ │ │ └── using_directives.rs │ │ │ ├── go.rs │ │ │ ├── go │ │ │ │ └── imports.rs │ │ │ ├── java.rs │ │ │ ├── java │ │ │ │ └── imports.rs │ │ │ ├── javascript.rs │ │ │ ├── javascript │ │ │ │ └── imports.rs │ │ │ ├── python.rs │ │ │ ├── python │ │ │ │ └── imports.rs │ │ │ ├── typescript.rs │ │ │ └── typescript │ │ │ │ └── imports.rs │ │ └── tree_sitter.rs │ │ ├── arguments.rs │ │ ├── classifiers.rs │ │ ├── classifiers │ │ └── tests.rs │ │ ├── config_file.rs │ │ ├── constants.rs │ │ ├── lib.rs │ │ ├── model.rs │ │ ├── model │ │ ├── analysis.rs │ │ ├── common.rs │ │ ├── config_file.rs │ │ ├── rule.rs │ │ ├── rule_test.rs │ │ ├── ruleset.rs │ │ └── violation.rs │ │ ├── path_restrictions.rs │ │ ├── rule_config.rs │ │ ├── rule_overrides.rs │ │ └── utils.rs └── static-analysis-server │ ├── Cargo.toml │ └── src │ ├── constants.rs │ ├── lib.rs │ ├── model.rs │ ├── model │ ├── analysis_request.rs │ ├── analysis_response.rs │ ├── tree_sitter_tree_node.rs │ ├── tree_sitter_tree_request.rs │ ├── tree_sitter_tree_response.rs │ └── violation.rs │ ├── request.rs │ └── tree_sitter_tree.rs ├── doc ├── diff-aware.md ├── docker-container.md ├── git-hooks.md ├── go.md ├── ignore-rule.md ├── imgs │ ├── git-hook.jpeg │ └── owasp.png ├── owasp-benchmark.md └── report-issue.md ├── misc ├── github-action.sh ├── helpers │ └── test-classification.sh ├── imgs │ ├── jetbrains.gif │ └── vscode.gif ├── integration-git-hooks.sh ├── integration-test-classification.sh ├── integration-test-default-config.sh ├── integration-test-docker.sh ├── integration-test-encoding.sh ├── integration-test-filter-rules.sh ├── integration-test-git.sh ├── integration-test-js-ts.sh ├── integration-test-python.sh ├── integration-test-r.sh ├── integration-test-rust.sh ├── integration-test-secrets.sh ├── integration-test-sql.sh └── test-rules.py ├── rust-toolchain.toml ├── schema ├── Makefile ├── README.md ├── examples │ ├── invalid │ │ ├── disable-ignore-generated-files.yml │ │ ├── gitignore-wrong-type.yml │ │ ├── ignore-wrong-type.yml │ │ ├── invalid-version.yml │ │ ├── maxfilesize-wrong-type.yml │ │ ├── only-wrong-type.yml │ │ ├── rules-arguments-wrong-type-list.yml │ │ ├── rules-arguments-wrong-type-map-of-list.yml │ │ ├── rules-arguments-wrong-type-string.yml │ │ ├── rules-category-invalid-value.yml │ │ ├── rules-ignore-wrong-type.yml │ │ ├── rules-only-wrong-type.yml │ │ ├── rules-severity-invalid-value.yml │ │ ├── rules-wrong-type-list.yml │ │ ├── rules-wrong-type-string.yml │ │ ├── rulesets-absent.yml │ │ ├── rulesets-empty.yml │ │ ├── rulesets-ignore-wrong-type.yml │ │ ├── rulesets-indentation.yml │ │ ├── rulesets-only-wrong-type.yml │ │ └── rulesets-wrong-type-map.yml │ └── valid │ │ ├── arguments.yml │ │ ├── complex1.yml │ │ ├── complex2.yml │ │ ├── disable-ignore-generated-files.yml │ │ ├── extensions.yml │ │ ├── severity.yml │ │ ├── simple.yml │ │ ├── with-gitignore.yml │ │ └── with-maxfilesize.yml └── schema.json └── versions.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | **/* @DataDog/k9-vm-ast 2 | /crates/bins/src/bin/datadog_static_analyzer_server/ide/**/* @DataDog/ide-integration-static-analysis-server 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/set-up-rust/action.yml: -------------------------------------------------------------------------------- 1 | name: Set Up Rust 2 | description: Installs the Rust toolchain 3 | 4 | inputs: 5 | target: 6 | description: The `target` to pass to rustup when installing the toolchain 7 | required: false 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - name: Install Rust toolchain 13 | uses: actions-rust-lang/setup-rust-toolchain@9d7e65c320fdb52dcd45ffaa68deb6c02c8754d9 # v1.12.0 14 | with: 15 | cache-bin: false 16 | target: ${{ inputs.target }} 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What problem are you trying to solve? 2 | 3 | ## What is your solution? 4 | 5 | ## Alternatives considered 6 | 7 | ## What the reviewer should know 8 | -------------------------------------------------------------------------------- /.github/scripts/dedup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes duplicate results from the given set of diffs. 3 | * The strategy is to keep the first occurrence of a ruleId and remove the rest. 4 | * 5 | * @param {Set} diffs 6 | * 7 | * @returns {object} An object containing the deduplicated set of diffs and a map of ruleIds to their count. 8 | */ 9 | export const dedup = (diffs) => { 10 | const seen = {}; 11 | const remove = []; 12 | for (const diff of diffs) { 13 | const json = JSON.parse(diff); 14 | if (seen[json.ruleId]) { 15 | remove.push(diff) 16 | } 17 | seen[json.ruleId] = (seen[json.ruleId] || 0) + 1; 18 | } 19 | for (const diff of remove) { 20 | diffs.delete(diff); 21 | } 22 | 23 | return [diffs, seen]; 24 | }; 25 | 26 | /** 27 | * Creates a set from the difference between two sets. 28 | * 29 | * @param {Set} set1 30 | * 31 | * @param {Set} set2 32 | * 33 | * @returns {Set} 34 | */ 35 | export const difference = (set1, set2) => { 36 | return new Set([...set1].filter(x => !set2.has(x))); 37 | } 38 | -------------------------------------------------------------------------------- /.github/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "check-regressions", 3 | "version": "1.0.0", 4 | "main": "check-regressions.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@actions/core": "1.10.1", 8 | "@actions/github": "^6.0.0" 9 | }, 10 | "scripts": { 11 | "test": "node --test test.js" 12 | }, 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/check-regressions.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "**" 5 | 6 | name: Check for regressions in static analysis kernel 7 | 8 | jobs: 9 | check_regressions: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | repo: 15 | - { org: "golang", name: "go" } 16 | - { org: "numpy", name: "numpy" } 17 | - { org: "npm", name: "cli" } 18 | - { org: "JamesNK", name: "Newtonsoft.Json" } 19 | - { org: "google", name: "guava" } 20 | - { org: "reduxjs", name: "redux" } 21 | - { org: "ruby", name: "spec" } 22 | - { org: "laravel", name: "framework" } 23 | - { org: "muh-nee", name: "BenchmarkJava" } 24 | - { org: "muh-nee", name: "NodeGoat" } 25 | - { org: "muh-nee", name: "WebGoat" } 26 | - { org: "muh-nee", name: "WebGoat.NET" } 27 | - { org: "muh-nee", name: "FlowBlot.NET" } 28 | - { org: "muh-nee", name: "Damn-Vulnerable-GraphQL-Application" } 29 | - { org: "muh-nee", name: "SecurityShepherd" } 30 | - { org: "muh-nee", name: "DSVW" } 31 | - { org: "muh-nee", name: "NIST-Juliet-CSharp-1.3" } 32 | - { org: "muh-nee", name: "DVWA" } 33 | env: 34 | DD_API_KEY: ${{ secrets.DD_API_KEY }} 35 | DD_APP_KEY: ${{ secrets.DD_APP_KEY }} 36 | DD_SITE: ${{ vars.DD_SITE }} 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | ref: main 41 | 42 | # This can be changed to the `set-up-rust` composite action after it lands on main. 43 | - name: Set up Rust 44 | uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 45 | 46 | - name: Checkout test repositories 47 | uses: actions/checkout@v4 48 | with: 49 | repository: ${{ matrix.repo.org }}/${{ matrix.repo.name }} 50 | path: ${{ matrix.repo.org }}/${{ matrix.repo.name }} 51 | 52 | - name: Fetch dependencies (before) 53 | run: cargo fetch 54 | 55 | - name: Obtain results before changes 56 | run: | 57 | cargo run --locked --release --bin datadog-static-analyzer -- -i ${{ matrix.repo.org }}/${{ matrix.repo.name }} -o result-pre.json -b -f sarif 58 | 59 | - name: Fetch all branches and checkout PR 60 | run: | 61 | git fetch --all 62 | git checkout ${{ github.sha }} 63 | echo 'checked out ${{ github.sha }}' 64 | 65 | - name: Fetch dependencies (after) 66 | run: cargo fetch 67 | 68 | - name: Obtain results after changes 69 | run: | 70 | cargo run --locked --release --bin datadog-static-analyzer -- -i ${{ matrix.repo.org }}/${{ matrix.repo.name }} -o result-post.json -b -f sarif 71 | 72 | - name: Install Node.js dependencies 73 | run: npm install 74 | working-directory: .github/scripts 75 | 76 | - name: Run Unit Tests 77 | run: npm test 78 | working-directory: .github/scripts 79 | 80 | - name: Run Regression Checks 81 | id: regression 82 | run: node ./.github/scripts/check-regressions.js ${{ matrix.repo.org }}/${{ matrix.repo.name }} result-pre.json result-post.json 83 | 84 | - name: Upload unique changes from before 85 | uses: actions/upload-artifact@v4 86 | if: steps.regression.outputs.diff1files != '' 87 | with: 88 | name: failures-before-${{ matrix.repo.org }}-${{ matrix.repo.name }} 89 | path: ${{ steps.regression.outputs.diff1files }} 90 | 91 | - name: Upload unique changes from after 92 | uses: actions/upload-artifact@v4 93 | if: steps.regression.outputs.diff2files != '' 94 | with: 95 | name: failures-after-${{ matrix.repo.org }}-${{ matrix.repo.name }} 96 | path: ${{ steps.regression.outputs.diff2files }} 97 | 98 | - name: Fail 99 | if: steps.regression.outputs.diff1files != '' || steps.regression.outputs.diff2files != '' 100 | run: exit 1 101 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | paths: 4 | - "Dockerfile" 5 | branches: 6 | - "**" 7 | pull_request: 8 | paths: 9 | - "Dockerfile" 10 | workflow_call: 11 | 12 | name: Test Docker Container 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Build Docker image 23 | run: docker build -t test-image . 24 | 25 | - name: Verify binary exists and runs 26 | run: | 27 | # Run container and execute the binaries to verify they exist and run 28 | docker run --rm test-image datadog-static-analyzer --help 29 | docker run --rm test-image datadog-static-analyzer-server --help 30 | docker run --rm test-image datadog-static-analyzer-git-hook --help 31 | -------------------------------------------------------------------------------- /.github/workflows/ghcr.yml: -------------------------------------------------------------------------------- 1 | name: Publish to GHCR 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | release: 7 | description: "Whether this is a release build or not" 8 | required: true 9 | default: false 10 | type: boolean 11 | 12 | env: 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: ${{ github.repository }} 15 | 16 | jobs: 17 | build-and-push: 18 | runs-on: ubuntu-24.04 19 | 20 | permissions: 21 | contents: read 22 | packages: write 23 | id-token: write 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | # Set the current SHA as the version so that it's exposed on the server. 30 | - name: Set the version 31 | shell: bash 32 | run: sed "s/development/$GITHUB_SHA/g" crates/static-analysis-kernel/src/constants.rs > bla && rm crates/static-analysis-kernel/src/constants.rs && mv bla crates/static-analysis-kernel/src/constants.rs 33 | 34 | - name: Set up QEMU 35 | uses: docker/setup-qemu-action@v3 36 | 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3 39 | 40 | - name: Login to GitHub Container Registry 41 | uses: docker/login-action@v3 42 | with: 43 | registry: ${{ env.REGISTRY }} 44 | username: ${{ github.actor }} 45 | password: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | - name: Extract metadata (tags, labels) for Docker 48 | id: meta 49 | uses: docker/metadata-action@v5 50 | with: 51 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 52 | flavor: | 53 | latest=${{ inputs.release }} 54 | tags: | 55 | type=raw,value=latest,enable=${{ inputs.release }} 56 | type=ref,event=tag 57 | type=sha 58 | 59 | - name: Build and push Docker image 60 | id: push 61 | uses: docker/build-push-action@v5 62 | with: 63 | context: . 64 | push: true 65 | tags: ${{ steps.meta.outputs.tags }} 66 | labels: ${{ steps.meta.outputs.labels }} 67 | platforms: linux/amd64,linux/arm64 68 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run Integration tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | pull_request: 8 | schedule: 9 | # run every day at 9am UTC 10 | - cron: '0 9 * * *' 11 | workflow_call: 12 | 13 | jobs: 14 | test_scripts: 15 | runs-on: ubuntu-latest 16 | env: 17 | DD_API_KEY: ${{ secrets.DD_API_KEY }} 18 | DD_APP_KEY: ${{ secrets.DD_APP_KEY }} 19 | DD_SITE: ${{ vars.DD_SITE }} 20 | strategy: 21 | matrix: 22 | scripts: 23 | - { file: './misc/integration-test-classification.sh', gha_alias: 'Artifact Classification' } 24 | - { file: './misc/integration-test-git.sh', gha_alias: 'Git' } 25 | - { file: './misc/integration-test-python.sh', gha_alias: 'Python' } 26 | - { file: './misc/integration-test-docker.sh', gha_alias: 'Docker' } 27 | - { file: './misc/integration-test-js-ts.sh', gha_alias: 'JavaScript/TypeScript' } 28 | - { file: './misc/integration-test-filter-rules.sh', gha_alias: 'Per-Path Rule Filtering' } 29 | - { file: './misc/integration-git-hooks.sh', gha_alias: 'Git Hooks' } 30 | - { file: './misc/integration-test-r.sh', gha_alias: 'R' } 31 | - { file: './misc/integration-test-rust.sh', gha_alias: 'Rust' } 32 | - { file: './misc/integration-test-sql.sh', gha_alias: 'SQL' } 33 | - { file: './misc/integration-test-encoding.sh', gha_alias: 'File Encoding' } 34 | - { file: './misc/integration-test-default-config.sh', gha_alias: 'Default Config' } 35 | name: Run integration test - ${{ matrix.scripts.gha_alias }} 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: ./.github/actions/set-up-rust 39 | - name: Execute script 40 | run: ${{ matrix.scripts.file }} 41 | -------------------------------------------------------------------------------- /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "**" 5 | pull_request: 6 | 7 | name: Check Code 8 | jobs: 9 | check_code: 10 | strategy: 11 | matrix: 12 | config: 13 | - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, gha_alias: 'Linux x64 - ' } 14 | - { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, gha_alias: 'Linux aarch64 - ' } 15 | - { os: macos-13, target: x86_64-apple-darwin, gha_alias: 'macOS x64 - ' } 16 | - { os: macos-latest, target: aarch64-apple-darwin, gha_alias: 'macOS aarch64 - ' } 17 | - { os: windows-latest, target: x86_64-pc-windows-msvc, gha_alias: 'Windows x64 - ' } 18 | cargo_cmd: 19 | - { cmd_name: build, args: '--locked' , gha_alias: "Build - Profile 'debug'" } 20 | - { cmd_name: test, args: '--workspace --locked', gha_alias: "Test" } 21 | include: 22 | - config: { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, gha_alias: '' } 23 | cargo_cmd: { cmd_name: clippy, args: '', gha_alias: "Clippy" } 24 | - config: { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, gha_alias: '' } 25 | cargo_cmd: { cmd_name: fmt, args: '--check', gha_alias: "Rustfmt" } 26 | name: ${{ matrix.config.gha_alias }}${{ matrix.cargo_cmd.gha_alias }} 27 | runs-on: ${{ matrix.config.os }} 28 | env: 29 | DD_API_KEY: ${{ secrets.DD_API_KEY }} 30 | DD_APP_KEY: ${{ secrets.DD_APP_KEY }} 31 | DD_SITE: ${{ vars.DD_SITE }} 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | 36 | - uses: ./.github/actions/set-up-rust 37 | 38 | - name: Fetch dependencies 39 | run: cargo fetch 40 | 41 | - name: Run cargo ${{ matrix.cargo_cmd.cmd_name }} ${{ matrix.cargo_cmd.args }} 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: ${{ matrix.cargo_cmd.cmd_name }} 45 | args: ${{ matrix.cargo_cmd.args }} 46 | 47 | - name: Check the lockfile is up to date 48 | if: ${{ matrix.config.target == 'aarch64-unknown-linux-gnu' }} 49 | run: | 50 | cargo check 51 | git diff --exit-code Cargo.lock || (echo "::error::Lockfile is out of date. Please run 'cargo check' and commit the updated Cargo.lock file." && exit 1) 52 | 53 | - name: Check python rulesets - part1 54 | run: cargo run --locked --bin datadog-static-analyzer-test-ruleset -- -r python-best-practices -r python-security -r python-code-style -r python-inclusive 55 | - name: Check python rulesets - part2 56 | run: cargo run --locked --bin datadog-static-analyzer-test-ruleset -- -r python-django -r python-flask -r python-design 57 | - name: Check Java rulesets 58 | run: cargo run --locked --bin datadog-static-analyzer-test-ruleset -- -r java-security -r java-best-practices -r java-code-style 59 | - name: Check Docker rulesets 60 | run: cargo run --locked --bin datadog-static-analyzer-test-ruleset -- -r docker-best-practices 61 | -------------------------------------------------------------------------------- /.github/workflows/sca.yml: -------------------------------------------------------------------------------- 1 | name: Software Composition Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | 8 | jobs: 9 | software-composition-analysis: 10 | runs-on: ubuntu-latest 11 | name: Datadog SBOM Generation and Upload 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - uses: ./.github/actions/set-up-rust 16 | - name: cargo install sbom 17 | run: cargo install --version 0.8.4 cargo-sbom 18 | - name: cargo generate sbom 19 | run: | 20 | cargo sbom --cargo-package static-analysis-kernel --output-format cyclone_dx_json_1_4 > static-analysis-kernel.json 21 | cargo sbom --cargo-package bins --output-format cyclone_dx_json_1_4 > bins.json 22 | cargo sbom --cargo-package cli --output-format cyclone_dx_json_1_4 > cli.json 23 | cargo sbom --cargo-package static-analysis-server --output-format cyclone_dx_json_1_4 > static-analysis-server.json 24 | - name: Generate SBOM and Upload 25 | id: software-composition-analysis 26 | uses: DataDog/datadog-sca-github-action@main 27 | with: 28 | dd_api_key: ${{ secrets.DD_API_KEY }} 29 | dd_app_key: ${{ secrets.DD_APP_KEY }} 30 | dd_service: datadog-static-analyzer 31 | dd_env: github-action 32 | dd_site: ${{ vars.DD_SITE }} 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/test-rules.yaml: -------------------------------------------------------------------------------- 1 | name: Test Rules (staging + prod) 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | pull_request: 8 | schedule: 9 | # run every day at 9am UTC 10 | - cron: '0 9 * * *' 11 | workflow_call: 12 | 13 | jobs: 14 | extract-languages: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | languages: ${{ steps.extract.outputs.languages }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - id: extract 21 | name: Extract languages from file 22 | run: | 23 | relative_path="crates/cli/src/datadog_utils.rs" 24 | 25 | if [ ! -f "$relative_path" ]; then 26 | echo "::error::Could not find file `$relative_path`" 27 | exit 1 28 | fi 29 | 30 | concat_languages=$( 31 | # Find the definition of the slice 32 | grep -Poz 'const DEFAULT_RULESETS_LANGUAGES: &\[&str\] = &\[\s*?(?:.*?\s*)+?\s*?\];' "$relative_path" | 33 | # Strip the null byte added by -z 34 | tr '\0' '\n' | 35 | # Get any strings present 36 | grep -Po '"[^"]*"' | 37 | # Delete quotation marks 38 | tr -d '"' | 39 | # Convert each newline to a space 40 | tr '\n' ' ' | 41 | # Strip a trailing space 42 | sed 's/ $//' 43 | ) 44 | 45 | if [ -z "$concat_languages" ]; then 46 | echo "::error::Could not parse default ruleset languages from file `$relative_path`" 47 | exit 1 48 | fi 49 | 50 | echo "languages=$concat_languages" >> $GITHUB_OUTPUT 51 | production_rules: 52 | needs: extract-languages 53 | runs-on: ubuntu-latest 54 | env: 55 | DD_SITE: datadoghq.com 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | - uses: ./.github/actions/set-up-rust 61 | 62 | - name: Fetch dependencies 63 | run: cargo fetch 64 | 65 | - name: Test all production rules 66 | run: | 67 | cargo build --locked --profile release-dev --bin datadog-static-analyzer && \ 68 | cargo build --locked --profile release-dev --bin datadog-static-analyzer-server && \ 69 | sudo apt-get install python3-requests && \ 70 | for language in ${{ needs.extract-languages.outputs.languages }}; do \ 71 | python misc/test-rules.py -c $PWD/target/release-dev/datadog-static-analyzer -s $PWD/target/release-dev/datadog-static-analyzer-server -l $language ; \ 72 | done 73 | 74 | staging_rules: 75 | needs: extract-languages 76 | runs-on: ubuntu-latest 77 | env: 78 | DD_SITE: datad0g.com 79 | steps: 80 | - name: Checkout repository 81 | uses: actions/checkout@v4 82 | 83 | - uses: ./.github/actions/set-up-rust 84 | 85 | - name: Fetch dependencies 86 | run: cargo fetch 87 | 88 | - name: Test all staging rules 89 | run: | 90 | cargo build --locked --profile release-dev --bin datadog-static-analyzer && \ 91 | cargo build --locked --profile release-dev --bin datadog-static-analyzer-server && \ 92 | sudo apt-get install python3-requests && \ 93 | for language in ${{ needs.extract-languages.outputs.languages }}; do \ 94 | python misc/test-rules.py -c $PWD/target/release-dev/datadog-static-analyzer -s $PWD/target/release-dev/datadog-static-analyzer-server -l $language ; \ 95 | done 96 | -------------------------------------------------------------------------------- /.github/workflows/verify-schema.yaml: -------------------------------------------------------------------------------- 1 | name: Check that the JSON schema for the configuration file matches the examples 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | workflow_call: 8 | 9 | jobs: 10 | test_json_schema: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Use Node.js 15 | uses: actions/setup-node@v4 16 | - run: npm install -g pajv 17 | - run: make -C schema 18 | -------------------------------------------------------------------------------- /.github/workflows/versions-check.yaml: -------------------------------------------------------------------------------- 1 | name: Check the versions.json file is correct 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | workflow_call: 8 | 9 | jobs: 10 | integration_tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Ensure versions 15 | run: python -mjson.tool versions.json >/dev/null 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # files from vim 13 | *~ 14 | 15 | # tree-sitter grammar sources 16 | crates/static-analysis-kernel/.vendor/ 17 | 18 | # IntelliJ 19 | .idea 20 | 21 | venv 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug 'datadog-static-analyzer-server'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=datadog-static-analyzer-server", 15 | "--package=datadog-static-analyzer" 16 | ], 17 | "filter": { 18 | "name": "datadog-static-analyzer-server", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": ["-p", "49159", "-e"], 23 | "env": { 24 | "RUST_LOG": "trace" 25 | }, 26 | "cwd": "${workspaceFolder}" 27 | }, 28 | { 29 | "type": "lldb", 30 | "request": "launch", 31 | "name": "Debug Tests in Server Binary", 32 | "cargo": { 33 | "args": ["test", "--no-run", "--package=datadog-static-analyzer"], 34 | "filter": { 35 | "name": "datadog-static-analyzer-server" 36 | } 37 | }, 38 | "args": [], 39 | "cwd": "${workspaceFolder}/crates/bins" 40 | }, 41 | { 42 | "type": "lldb", 43 | "request": "launch", 44 | "name": "Debug Tests in Kernel", 45 | "cargo": { 46 | "args": [ 47 | "test", 48 | "--no-run", 49 | "--lib", 50 | "--package=static-analysis-kernel" 51 | ], 52 | "filter": { 53 | "name": "static-analysis-kernel", 54 | "kind": "lib" 55 | } 56 | }, 57 | "args": [], 58 | "cwd": "${workspaceFolder}/crates/static-analysis-kernel" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.allFeatures": false, 3 | "rust-analyzer.showUnlinkedFileNotification": false 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Code Organization 4 | 5 | The code is organized into four crates: 6 | 7 | - `static-analysis-kernel`: the core of the analyzer that is used either by the server or the command-line 8 | - `cli`: code only for the command-line interface (e.g. get rules from API/json) 9 | - `static-analysis-server`: code only for the server (e.g. receiving requests and send back response) 10 | - `bins`: code for the binaries that references code in `cli` or `static-analysis-erver` 11 | 12 | ## Code Quality 13 | 14 | There is a git hook set up with [cargo husky](https://lib.rs/crates/cargo-husky) 15 | that checks your code complies with good coding guidelines. 16 | 17 | It runs clippy, rust-fmt and tests before commit and pushing code. 18 | 19 | There is also a [GitHub action](.github/workflows/rust.yaml) set up to 20 | enforce code quality rules once the code is committed. 21 | 22 | ## Contribute 23 | 24 | Please contribute any way you want. It may be by submitting new issues, 25 | making a pull request. 26 | 27 | Some rules to contribute to the project: 28 | 29 | - any code change or new feature must have a change 30 | - always enforce the rule of least requirement. In other words, do not put in `static-analysis-kernel` code that is related only to the `cli` 31 | - all tests and checks must pass 32 | - please be respectful of other projects contributors 33 | - if you add a new dependency, add the relevant information in the file `LICENSE-3rdparty.csv` -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*"] 3 | default-members = [ 4 | "crates/bins", 5 | "crates/cli", 6 | "crates/static-analysis-kernel", 7 | "crates/static-analysis-server", 8 | "crates/secrets", 9 | ] 10 | resolver = "2" 11 | 12 | [workspace.package] 13 | version = "0.6.6" 14 | 15 | [profile.release] 16 | lto = true 17 | 18 | [profile.release-dev] 19 | inherits = "release" 20 | lto = false 21 | debug = true 22 | debug-assertions = true 23 | overflow-checks = true 24 | incremental = true 25 | codegen-units = 256 26 | 27 | [workspace.dependencies] 28 | anyhow = "1" 29 | base64 = "0.21.2" 30 | dashmap = "6.1.0" 31 | git2 = "0.19.0" 32 | indexmap = { version = "2.9", features = ["serde"] } 33 | itertools = "0.14.0" 34 | derive_builder = "0.12" 35 | serde = { version = "1", features = ["derive"] } 36 | serde_json = "1.0.140" 37 | serde-sarif = "0.4" 38 | serde_yaml = "0.9.21" 39 | sha2 = "0.10.9" 40 | num_cpus = "1.15.0" 41 | thiserror = "2.0.12" 42 | tracing = "0.1.40" 43 | uuid = { version = "1.17.0", features = ["v4"] } 44 | tree-sitter = "0.24.7" 45 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | 2 | ## Build the project 3 | 4 | You need to have all the necessary [tree-sitter](https://github.com/tree-sitter) repositories 5 | in `kernel` for the analysis to work. The `build` process will take care of this for you: 6 | 7 | ```shell 8 | cargo build --profile release-dev 9 | ``` 10 | > [!TIP] 11 | > The use of the `release-dev` cargo profile is highly recommended, as runtime analysis speed is effectively 12 | > the same as `release` mode, but compile times are comparable to `debug` mode. 13 | 14 | ## Analyze a directory 15 | 16 | ```shell 17 | 18 | cargo run --profile release-dev --bin datadog-static-analyzer -- --directory --output result.json --format sarif --debug yes 19 | ``` 20 | 21 | ## Start a local server 22 | 23 | ```shell 24 | cargo run --profile release-dev --bin datadog-static-analyzer-server -- --port -a 25 | ``` 26 | 27 | ## Run tests 28 | 29 | Run all tests 30 | 31 | ```shell 32 | cargo test 33 | ``` 34 | 35 | 36 | ## Test a ruleset 37 | 38 | ```shell 39 | cargo run --bin datadog-static-analyzer-test-ruleset -- -r 1 40 | ``` 41 | 42 | 43 | ## Run tests with output 44 | 45 | ```shell 46 | cargo test -- --nocapture 47 | ``` 48 | 49 | 50 | ## Test a request to the server 51 | 52 | First, start the server using 53 | 54 | ```shell 55 | cargo run --profile release-dev --bin datadog-static-analyzer-server 56 | ``` 57 | 58 | 59 | ### Get an analysis request 60 | ```shell 61 | 62 | curl -X POST \ 63 | -H "Content-Type: application/json" \ 64 | --data '{ 65 | "filename": "myfile.py", 66 | "file_encoding": "utf-8", 67 | "language": "PYTHON", 68 | "code": "ZGVmIGZvbyhhcmcxKToKICAgIHBhc3M=", 69 | "rules": [ 70 | { 71 | "id": "myrule", 72 | "short_description": "", 73 | "description": "", 74 | "language": "PYTHON", 75 | "type": "TREE_SITTER_QUERY", 76 | "entity_checked": null, 77 | "code": "ZnVuY3Rpb24gdmlzaXQobm9kZSwgZmlsZW5hbWUsIGNvZGUpIHsKICAgIGNvbnN0IGZ1bmN0aW9uTmFtZSA9IG5vZGUuY2FwdHVyZXNbIm5hbWUiXTsKICAgIGlmKGZ1bmN0aW9uTmFtZSkgewogICAgICAgIGNvbnN0IGVycm9yID0gYnVpbGRFcnJvcihmdW5jdGlvbk5hbWUuc3RhcnQubGluZSwgZnVuY3Rpb25OYW1lLnN0YXJ0LmNvbCwgZnVuY3Rpb25OYW1lLmVuZC5saW5lLCBmdW5jdGlvbk5hbWUuZW5kLmNvbCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImludmFsaWQgbmFtZSIsICJDUklUSUNBTCIsICJzZWN1cml0eSIpOwoKICAgICAgICBjb25zdCBlZGl0ID0gYnVpbGRFZGl0KGZ1bmN0aW9uTmFtZS5zdGFydC5saW5lLCBmdW5jdGlvbk5hbWUuc3RhcnQuY29sLCBmdW5jdGlvbk5hbWUuZW5kLmxpbmUsIGZ1bmN0aW9uTmFtZS5lbmQuY29sLCAidXBkYXRlIiwgImJhciIpOwogICAgICAgIGNvbnN0IGZpeCA9IGJ1aWxkRml4KCJ1c2UgYmFyIiwgW2VkaXRdKTsKICAgICAgICBhZGRFcnJvcihlcnJvci5hZGRGaXgoZml4KSk7CiAgICB9Cn0=", 78 | "tree_sitter_query": "KGZ1bmN0aW9uX2RlZmluaXRpb24KICAgIG5hbWU6IChpZGVudGlmaWVyKSBAbmFtZQogIHBhcmFtZXRlcnM6IChwYXJhbWV0ZXJzKSBAcGFyYW1zCik=" 79 | } 80 | ] 81 | }' \ 82 | http://localhost:8000/analyze 83 | ``` 84 | 85 | ### Get the AST Tree 86 | 87 | ```shell 88 | curl -X POST \ 89 | -H "Content-Type: application/json" \ 90 | --data '{ 91 | "file_encoding": "utf-8", 92 | "language": "PYTHON", 93 | "code": "ZGVmIGZvbyhhcmcxKToKICAgIHBhc3M=" 94 | }' \ 95 | http://localhost:8000/get-treesitter-ast 96 | ``` 97 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 AS base 2 | 3 | FROM base AS build 4 | 5 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 6 | 7 | ENV RUSTUP_HOME=/usr/local/rustup \ 8 | CARGO_HOME=/usr/local/cargo \ 9 | PATH=/usr/local/cargo/bin:$PATH 10 | 11 | RUN apt-get update && apt-get --no-install-recommends install -y \ 12 | build-essential ca-certificates curl git 13 | 14 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y \ 15 | && rustup --version \ 16 | && cargo --version \ 17 | && rustc --version 18 | 19 | COPY . /app 20 | WORKDIR /app 21 | RUN cargo fetch 22 | RUN cargo build --locked --release --bin datadog-static-analyzer 23 | RUN cargo build --locked --release --bin datadog-static-analyzer-git-hook 24 | RUN cargo build --locked --release --bin datadog-static-analyzer-server 25 | 26 | FROM base 27 | 28 | COPY --from=build /app/target/release/datadog-static-analyzer /usr/bin/datadog-static-analyzer 29 | COPY --from=build /app/target/release/datadog-static-analyzer-server /usr/bin/datadog-static-analyzer-server 30 | COPY --from=build /app/target/release/datadog-static-analyzer-git-hook /usr/bin/datadog-static-analyzer-git-hook 31 | COPY --from=build /app/misc/github-action.sh /usr/bin/github-action.sh 32 | 33 | RUN apt update && apt install -y curl git \ 34 | && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ 35 | && apt remove -y nodejs npm \ 36 | && apt install -y nodejs \ 37 | && apt clean && rm -rf /var/lib/apt/lists/* 38 | RUN npm install -g @datadog/datadog-ci \ 39 | && datadog-ci --version \ 40 | && datadog-static-analyzer --version 41 | 42 | ENTRYPOINT ["/usr/bin/datadog-static-analyzer"] 43 | CMD ["--help"] 44 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | ## Generating the SARIF output file is slow 2 | 3 | This can occur is you are using the option `-g` and generate a SARIF file. The `-g` option 4 | attempts to get the last commit for a file/line. In some case, git can be very slow and the 5 | generation of the file can take a long time. 6 | 7 | If you try to generate a SARIF file with the `-g` and have a slow file generation, consider 8 | trying without the `-g` flag. 9 | 10 | ## How to make the analyzer exit if there is at least one violation? 11 | 12 | Use the `--fail-on-any-violation` option. 13 | 14 | To make the analyzer exit if there is at least one violation at any level, use the following command 15 | 16 | ```shell 17 | datadog-static-analyzer --directory /path/to/code -o results.json -f sarif --fail-on-any-violation=none,notice,warning,error 18 | ``` 19 | 20 | To exit only if you have a violation with error, use: 21 | 22 | ```shell 23 | datadog-static-analyzer --directory /path/to/code -o results.json -f sarif --fail-on-any-violation=error 24 | ``` 25 | 26 | ## Do you support Alpine Linux/musl libc? 27 | 28 | If you tried to run the analyzer on Alpine and/or got the following error: 29 | 30 | ``` 31 | Error relocating /lib/ld-linux-x86-64.so.2: unsupported relocation 32 | ``` 33 | 34 | it means you are not using glibc. Unfortunately, we do not support Alpine Linux 35 | at this time. We plan to support it in the future, the issue is tracked [here](https://github.com/DataDog/datadog-static-analyzer/issues/245). 36 | 37 | 38 | ## How to produce a CSV file? 39 | 40 | Use the `--format csv` option like this 41 | 42 | ```shell 43 | datadog-static-analyzer --directory /path/to/code -o results.csv -f csv 44 | ``` 45 | 46 | ## Diff-aware scan fails 47 | 48 | If you get the error `diff aware not enabled (unable to generate diff-aware request data), proceeding with full scan`, it 49 | is generally because the user running the scan and the user owning the repository are different. 50 | 51 | To fix this issue you can disable the `safe_directory` Git option. To do so, run the following command: 52 | 53 | ```shell 54 | 55 | git config --global --add safe.directory /path/to/repository 56 | 57 | ``` 58 | 59 | You can learn more about diff-aware scanning in our [dedicated documentation](doc/diff-aware.md). 60 | 61 | 62 | ## How to scan generated files? 63 | 64 | By default, the static analyzer skip generated files (like files generated by protobuf). 65 | This is implemented in [generated_content.rs](crates/static-analysis-kernel/src/analysis/generated_content.rs). 66 | 67 | If you want to force the analysis of generated code, add the directive 68 | `ignore-generated-files` in your `static-analysis.datadog.yml` file as shown below. 69 | 70 | ```yaml 71 | rulesets: 72 | - ruleset1 73 | ignore-generated-files: false 74 | ``` 75 | 76 | 77 | ## I have a bug to report 78 | 79 | Please send us a bug report, either by [creating an issue](https://github.com/DataDog/datadog-static-analyzer/issues) 80 | on this repository or by contacting your Datadog Customer Success Manager (if you are a Datadog customer). 81 | 82 | You can find complete information on how to report a bug in the 83 | [dedicated documentation page](doc/report-issue.md). 84 | 85 | 86 | ## I do not see an answer to my question 87 | 88 | Please ask your question in the [discussions section](https://github.com/DataDog/datadog-static-analyzer/discussions). 89 | 90 | -------------------------------------------------------------------------------- /LICENSE-3rdparty.csv: -------------------------------------------------------------------------------- 1 | Component,Origin,License,Copyright 2 | anyhow,https://crates.io/crates/anyhow,MIT,Copyright (c) 2019 David Tolnay 3 | base64,https://github.com/marshallpierce/rust-base64,Apache-2.0,Copyright (c) 2015 Alice Maz 4 | bstr,https://github.com/BurntSushi/bstr,MIT,Copyright (c) 2018-2019 Andrew Gallant 5 | csv,https://github.com/BurntSushi/rust-csv,MIT,Copyright (c) 2015 Andrew Gallant 6 | dashmap,https://github.com/xacrimon/dashmap,MIT,Copyright (c) 2019 Acrimon 7 | deno-core,https://github.com/denoland/deno,MIT,Copyright 2018-2023 the Deno authors 8 | futures,https://rust-lang.github.io/futures-rs/,MIT,Copyright (c) 2017 The Tokio Authors 9 | git2,https://crates.io/crates/git2,MIT,Copyright (c) 2014 Alex Crichton 10 | globset,https://crates.io/crates/globset,MIT,Copyright (c) 2015 Andrew Gallant 11 | graphviz-rust,https://github.com/besok/graphviz-rust,MIT,Copyright (c) 2013 Boris Zhguchev 12 | indexmap,https://github.com/indexmap-rs/indexmap,MIT and Apache-2.0,Copyright (c) 2016-2017 bluss 13 | indicatif,https://crates.io/crates/indicatif,MIT,Copyright (c) 2017 Armin Ronacher 14 | itertools,https://github.com/rust-itertools/itertools,MIT,Copyright 2015 itertools Developers 15 | num_cpus,https://github.com/seanmonstar/num_cpus,MIT, Copyright (c) 2015 Sean McArthur 16 | percent-encoding,https://github.com/servo/rust-url/,MIT, Copyright (c) 2013-2022 The rust-url developers 17 | prettytable-rs,https://github.com/phsym/prettytable-rs/,BSD-3-Clause,Copyright (c) 2022 Pierre-Henri Symoneaux 18 | rayon,https://crates.io/crates/rayon,MIT,Copyright (c) 2010 The Rust Project Developers 19 | path_slash,https://github.com/rhysd/path-slash,MIT,Copyright (c) 2018 rhysd 20 | pretty_yaml,https://github.com/g-plane/pretty_yaml,MIT,Copyright (c) 2024-present Pig Fang 21 | rocket,https://github.com/SergioBenitez/Rocket,Apache-2.0,Copyright 2016 Sergio Benitez 22 | tree-sitter,https://github.com/tree-sitter/tree-sitter,MIT,2014 Max Brunsfeld 23 | sarif-rs,https://github.com/psastras/sarif-rs,MIT,Copyright (c) 2021 Paul Sastrasinh 24 | sequence_trie,https://docs.rs/sequence_trie/,MIT,Copyright (c) 2016 Michael Sproul and contributors. 25 | serde,https://github.com/serde-rs/serde,Apache-2.0,2015 David Tolnay and Serde contributors 26 | serde_json,https://github.com/serde-rs/json,Apache-2.0,2015 David Tolnay and Serde contributors 27 | serde_yaml,https://github.com/dtolnay/serde-yaml,Apache-2.0,2016 David Tolnay 28 | sha2,https://crates.io/crates/sha2,Apache-2.0,Copyright (c) 2006-2009 Graydon Hoare 2009-2013 Mozilla Foundation 2016 Artyom Pavlov 29 | streaming-iterator,https://crates.io/crates/streaming-iterator,MIT or Apache-2.0,Copyright (c) 2016 Steven Fackler 30 | tempfile,https://crates.io/crates/tempfile,Apache-2.0,Copyright (c) 2015 Steven Allen 31 | terminal-emoji,https://github.com/mainrs/terminal-emoji-rs,MIT,Copyright (c) 2020 SirWindfield 32 | thiserror,https://github.com/dtolnay/thiserror,MIT,Copyright (c) 2019 David Tolnay 33 | tracing,https://crates.io/crates/tracing,MIT,Copyright (c) 2019 Tokio Contributors 34 | tracing-subscriber,https://crates.io/crates/tracing-subscriber,MIT,Copyright (c) 2019 Tokio Contributors 35 | uuid,https://crates.io/crates/uuid,MIT,Copyright 2018 The Uuid Developers 36 | valico,https://github.com/s-panferov/valico,MIT,Copyright (c) 2014 Stanislav Panferov 37 | walkdir,https://github.com/BurntSushi/walkdir,MIT,Copyright (c) 2015 Andrew Gallant 38 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Datadog datadog-static-analyzer 2 | Copyright 2023 Datadog, Inc. 3 | 4 | This product includes software developed at Datadog = env::args().collect(); 18 | let program = args[0].clone(); 19 | let mut opts = Options::new(); 20 | opts.optopt("o", "output", "output file", "rulesets.json"); 21 | opts.optmulti("r", "ruleset", "ruleset to fetch", "python-security"); 22 | opts.optflag("h", "help", "print this help"); 23 | opts.optflag("v", "version", "shows the version"); 24 | opts.optflag("s", "staging", "use staging"); 25 | 26 | let matches = match opts.parse(&args[1..]) { 27 | Ok(m) => m, 28 | Err(f) => { 29 | panic!("error when parsing arguments: {}", f) 30 | } 31 | }; 32 | 33 | if matches.opt_present("v") { 34 | println!("{}", VERSION); 35 | exit(1); 36 | } 37 | 38 | if matches.opt_present("h") { 39 | print_usage(&program, opts); 40 | exit(1); 41 | } 42 | 43 | if !matches.opt_present("o") { 44 | eprintln!("--output not defined"); 45 | print_usage(&program, opts); 46 | exit(1); 47 | } 48 | 49 | if !matches.opt_present("r") { 50 | eprintln!("--ruleset not defined"); 51 | print_usage(&program, opts); 52 | exit(1); 53 | } 54 | 55 | let use_staging = matches.opt_present("s"); 56 | let rulesets_names = matches.opt_strs("r"); 57 | let file_to_write = matches.opt_str("o").expect("output file"); 58 | 59 | // get the rulesets from the API 60 | let rulesets: Vec = rulesets_names 61 | .iter() 62 | .map(|ruleset_name| { 63 | get_ruleset(ruleset_name, use_staging, true).expect("error when reading ruleset") 64 | }) 65 | .collect(); 66 | 67 | let file = File::create(&file_to_write).expect("error when opening the output file"); 68 | let mut writer = BufWriter::new(file); 69 | serde_json::to_writer(&mut writer, &rulesets).expect("error when writing the file"); 70 | writer.flush().expect("error when writing the file"); 71 | println!( 72 | "rulesets {} saved in file {}", 73 | rulesets_names.join(","), 74 | file_to_write 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog-static-analyzer-server.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use crate::datadog_static_analyzer_server::rule_cache::RuleCache; 6 | use kernel::analysis::ddsa_lib::v8_platform::{initialize_v8, Initialized, V8Platform}; 7 | use std::sync::OnceLock; 8 | 9 | mod datadog_static_analyzer_server; 10 | 11 | pub(crate) static V8_PLATFORM: OnceLock> = OnceLock::new(); 12 | pub(crate) static RAYON_POOL: OnceLock = OnceLock::new(); 13 | pub(crate) static RULE_CACHE: OnceLock = OnceLock::new(); 14 | 15 | #[rocket::main] 16 | async fn main() { 17 | // NOTE: It's imperative that the Rayon pool (which will handle analysis jobs that use v8) 18 | // is created by the same thread that initializes v8 (see the documentation 19 | // on the `initialize_v8` function for more information). 20 | let v8 = initialize_v8(0); 21 | V8_PLATFORM.set(v8).expect("cell should have been unset"); 22 | 23 | // This must be called _after_ `initialize_v8` (otherwise, PKU-related segfaults on Linux will occur). 24 | let rayon_pool = rayon::ThreadPoolBuilder::new() 25 | .num_threads(0) 26 | .build() 27 | .expect("rayon pool should be buildable"); 28 | RAYON_POOL 29 | .set(rayon_pool) 30 | .expect("cell should have been unset"); 31 | 32 | datadog_static_analyzer_server::start().await; 33 | } 34 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/error_codes.rs: -------------------------------------------------------------------------------- 1 | pub const ERROR_GENERAL: i32 = 1; 2 | pub const ERROR_BAD_ADDRESS: i32 = 14; 3 | pub const ERROR_INVALID_ARGUMENT: i32 = 22; 4 | pub const ERROR_CHANNEL_DISCONNECTED: i32 = 100; 5 | pub const ERROR_CHANNEL_SENDER_DROPPED: i32 = 102; 6 | pub const ERROR_SHUTDOWN_FAILURE: i32 = 103; 7 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/ide/Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.dev] 2 | workspace = false 3 | command = "cargo" 4 | watch = true 5 | env = { RUST_LOG = "off,datadog_static_analyzer=debug" } 6 | args = [ 7 | "run", 8 | "-p", 9 | "datadog-static-analyzer", 10 | "--bin", 11 | "datadog-static-analyzer-server", 12 | "--", 13 | "-p", 14 | "49159", 15 | "-c", 16 | "--rule-timeout-ms", 17 | "500", 18 | ] 19 | 20 | [tasks.ide-test] 21 | workspace = false 22 | command = "cargo" 23 | watch = true 24 | args = ["test", "-p", "datadog-static-analyzer", "ide", "--", "--nocapture"] 25 | 26 | [tasks.udeps] 27 | workspace = false 28 | install_crate = "udeps" 29 | toolchain = "nightly" 30 | command = "cargo" 31 | args = ["udeps", "--all-targets"] 32 | 33 | [tasks.format] 34 | clear = true 35 | workspace = false 36 | install_crate = "rustfmt" 37 | command = "cargo" 38 | args = ["fmt", "--all", "--", "--check"] 39 | 40 | [tasks.clippy] 41 | workspace = false 42 | install_crate = "cargo-clippy" 43 | command = "cargo" 44 | args = ["clippy"] 45 | 46 | [tasks.all] 47 | workspace = false 48 | dependencies = ["check", "format", "clippy"] 49 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/ide/configuration_file/comment_preserver/mod.rs: -------------------------------------------------------------------------------- 1 | mod models; 2 | mod reconciler; 3 | pub use reconciler::{prettify_yaml, reconcile_comments, ReconcileError}; 4 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/ide/configuration_file/comment_preserver/models.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Default)] 2 | pub struct Line { 3 | pub row: usize, 4 | pub content: String, 5 | } 6 | 7 | impl Line { 8 | pub const fn new(row: usize, content: String) -> Self { 9 | Self { row, content } 10 | } 11 | } 12 | 13 | #[derive(Debug, Clone)] 14 | pub enum Comment { 15 | Inline { 16 | line: Line, 17 | original_content: String, 18 | }, 19 | Block { 20 | line: Line, 21 | above_line: Option, 22 | below_line: Option, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/ide/configuration_file/endpoints.rs: -------------------------------------------------------------------------------- 1 | use super::error::ConfigFileError; 2 | use super::models::{AddRuleSetsRequest, IgnoreRuleRequest}; 3 | use super::static_analysis_config_file::StaticAnalysisConfigFile; 4 | use kernel::utils::encode_base64_string; 5 | use rocket::http::Status; 6 | use rocket::response::status::Custom; 7 | use rocket::serde::json::Json; 8 | use tracing::instrument; 9 | 10 | #[instrument()] 11 | #[rocket::post( 12 | "/v1/config/ignore-rule", 13 | format = "application/json", 14 | data = "" 15 | )] 16 | pub fn ignore_rule(request: Json) -> Result> { 17 | let IgnoreRuleRequest { 18 | rule, 19 | configuration_base64, 20 | encoded, 21 | .. 22 | } = request.into_inner(); 23 | tracing::debug!(rule, content = &configuration_base64); 24 | let result = StaticAnalysisConfigFile::with_ignored_rule(rule.into(), configuration_base64); 25 | to_response_result(result, encoded) 26 | } 27 | 28 | #[instrument()] 29 | #[rocket::post("/v1/config/rulesets", format = "application/json", data = "")] 30 | pub fn post_rulesets(request: Json) -> Result> { 31 | let AddRuleSetsRequest { 32 | rulesets, 33 | configuration_base64, 34 | encoded, 35 | .. 36 | } = request.into_inner(); 37 | tracing::debug!( 38 | rulesets=?&rulesets, 39 | content=&configuration_base64 40 | ); 41 | let result = StaticAnalysisConfigFile::with_added_rulesets(&rulesets, configuration_base64); 42 | to_response_result(result, encoded) 43 | } 44 | 45 | #[instrument()] 46 | #[rocket::get("/v1/config/rulesets/")] 47 | pub fn get_rulesets(content: &str) -> Json> { 48 | tracing::debug!(%content); 49 | Json(StaticAnalysisConfigFile::to_rulesets(content.to_string())) 50 | } 51 | 52 | #[instrument()] 53 | #[rocket::get("/v1/config/can-onboard/")] 54 | pub fn can_onboard(content: &str) -> Result, Custom> { 55 | tracing::debug!(%content); 56 | let config = StaticAnalysisConfigFile::try_from(content.to_string()) 57 | .map_err(|e| Custom(Status::InternalServerError, e))?; 58 | let can_onboard = config.is_onboarding_allowed(); 59 | Ok(Json(can_onboard)) 60 | } 61 | 62 | fn to_response_result( 63 | result: Result, 64 | encode: bool, 65 | ) -> Result> { 66 | result 67 | .map(|r| if encode { encode_base64_string(r) } else { r }) 68 | .map_err(|e| Custom(Status::InternalServerError, e)) 69 | } 70 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/ide/configuration_file/error.rs: -------------------------------------------------------------------------------- 1 | use super::comment_preserver::ReconcileError; 2 | use rocket::{http::ContentType, response::Responder, Response}; 3 | use serde_json::json; 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Error)] 7 | #[error("Static Analysis Config file error")] 8 | pub enum ConfigFileError { 9 | #[error("Error parsing yaml file")] 10 | Parser { 11 | #[from] 12 | source: serde_yaml::Error, 13 | }, 14 | #[error("Error decoding base64 string")] 15 | Decoder { source: anyhow::Error }, 16 | 17 | #[error("Error reconciling comments")] 18 | CommentReconciler { 19 | #[from] 20 | source: ReconcileError, 21 | }, 22 | } 23 | 24 | impl From for ConfigFileError { 25 | fn from(value: anyhow::Error) -> Self { 26 | match value.downcast::() { 27 | Ok(e) => Self::Parser { source: e }, 28 | Err(e) => Self::Decoder { source: e }, 29 | } 30 | } 31 | } 32 | 33 | impl<'r> Responder<'r, 'static> for ConfigFileError { 34 | fn respond_to(self, request: &'r rocket::Request<'_>) -> rocket::response::Result<'static> { 35 | let json = json!({"error": self.to_string(), "code": self.code()}); 36 | Response::build_from(json.respond_to(request)?) 37 | .header(ContentType::JSON) 38 | .ok() 39 | } 40 | } 41 | 42 | impl ConfigFileError { 43 | pub const fn code(&self) -> u16 { 44 | match self { 45 | Self::Parser { .. } => 1, 46 | Self::Decoder { .. } => 2, 47 | Self::CommentReconciler { .. } => 3, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/ide/configuration_file/mod.rs: -------------------------------------------------------------------------------- 1 | mod comment_preserver; 2 | pub mod endpoints; 3 | mod error; 4 | mod models; 5 | mod static_analysis_config_file; 6 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/ide/configuration_file/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] 4 | pub struct IgnoreRuleRequest { 5 | pub rule: String, 6 | #[serde(rename = "configuration")] 7 | pub configuration_base64: String, 8 | pub encoded: bool, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] 12 | pub struct AddRuleSetsRequest { 13 | pub rulesets: Vec, 14 | #[serde(rename = "configuration")] 15 | pub configuration_base64: Option, 16 | pub encoded: bool, 17 | } 18 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/ide/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_name_repetitions)] 2 | 3 | mod configuration_file; 4 | use rocket::Route; 5 | 6 | pub fn ide_routes() -> Vec { 7 | rocket::routes![ 8 | configuration_file::endpoints::ignore_rule, 9 | configuration_file::endpoints::post_rulesets, 10 | configuration_file::endpoints::get_rulesets, 11 | configuration_file::endpoints::can_onboard 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/mod.rs: -------------------------------------------------------------------------------- 1 | use std::process; 2 | 3 | use cli::{CliError, RocketPreparation}; 4 | use endpoints::EndpointError; 5 | use error_codes::{ 6 | ERROR_BAD_ADDRESS, ERROR_CHANNEL_SENDER_DROPPED, ERROR_GENERAL, ERROR_INVALID_ARGUMENT, 7 | }; 8 | use rocket::tokio::sync::mpsc::channel; 9 | 10 | mod cli; 11 | mod endpoints; 12 | mod error_codes; 13 | mod fairings; 14 | mod ide; 15 | pub(crate) mod rule_cache; 16 | mod state; 17 | mod utils; 18 | 19 | /// Starts the process 20 | /// 21 | /// # Panics 22 | /// 23 | /// This function will exit the process and panic when it finds an error. 24 | pub async fn start() { 25 | let (tx_keep_alive_error, mut rx_keep_alive_error) = channel::(1); 26 | 27 | // prepare the rocket based on the cli args. 28 | // NOTE: shared state is already managed by the rocket. No need to use `manage` again. 29 | // we get the state back just in case we want to add a particular fairing based on it. 30 | // IMPORTANT: we're passing a clone of the tx as we want to keep a reference in the 31 | // main thread to avoid the rx from returning None if the tx is dropped in cli::prepare_rocket. 32 | // This will happen for sure in case the keep-alive mechanism is not enabled. 33 | // Just by keeping the reference in here, we are good. 34 | let rocket_preparation = cli::prepare_rocket(tx_keep_alive_error.clone()); 35 | match rocket_preparation { 36 | Err(e) => { 37 | eprintln!("Error found: {e}"); 38 | match e { 39 | CliError::Parsing(_) => process::exit(ERROR_INVALID_ARGUMENT), 40 | _ => process::exit(ERROR_GENERAL), 41 | } 42 | } 43 | Ok(RocketPreparation::NoServerInteraction) => { 44 | // don't do anything, just exit with 0 code 45 | } 46 | Ok(RocketPreparation::ServerInfo { 47 | mut rocket, 48 | state, 49 | tx_rocket_shutdown, 50 | guards, 51 | }) => { 52 | // set fairings 53 | *rocket = rocket 54 | .attach(fairings::Cors) 55 | .attach(fairings::CustomHeaders) 56 | .attach(fairings::TracingFairing); 57 | if state.is_keepalive_enabled { 58 | *rocket = rocket.attach(fairings::KeepAlive); 59 | } 60 | 61 | // launch the rocket and check if we receive a keep-alive error 62 | let result = rocket::tokio::select! { 63 | a = endpoints::launch_rocket_with_endpoints(*rocket, tx_rocket_shutdown) => a, 64 | b = rx_keep_alive_error.recv() => b.map_or_else(|| Err(EndpointError::ExitCode(ERROR_CHANNEL_SENDER_DROPPED)), |c| Err(c.into())), 65 | }; 66 | 67 | if let Err(e) = result { 68 | let error_str = format!("{e:?}"); 69 | 70 | tracing::error!( 71 | "Something went wrong while trying to ignite the rocket: {error_str}" 72 | ); 73 | // flushing the pending logs by dropping the guard before panic 74 | for guard in guards { 75 | drop(guard); 76 | } 77 | 78 | match e { 79 | EndpointError::ExitCode(code) => process::exit(code), 80 | EndpointError::RocketError(re) => { 81 | if let rocket::error::ErrorKind::Bind(_) = re.kind() { 82 | process::exit(ERROR_BAD_ADDRESS); 83 | } 84 | } 85 | _ => (), 86 | } 87 | 88 | panic!("Something went wrong while trying to ignite the rocket {error_str}"); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/state.rs: -------------------------------------------------------------------------------- 1 | use super::utils::get_current_timestamp_ms; 2 | use std::{ 3 | sync::{Arc, RwLock}, 4 | time::Duration, 5 | }; 6 | 7 | #[derive(Clone)] 8 | pub struct ServerState { 9 | pub last_ping_request_timestamp_ms: Arc>, 10 | pub static_directory: Option, 11 | pub is_shutdown_enabled: bool, 12 | pub is_keepalive_enabled: bool, 13 | pub is_rule_cache_enabled: bool, 14 | pub rule_timeout_ms: Option, 15 | } 16 | 17 | impl ServerState { 18 | pub fn new( 19 | static_directory: Option, 20 | is_shutdown_enabled: bool, 21 | timeout: Option, 22 | ) -> Self { 23 | Self { 24 | last_ping_request_timestamp_ms: Arc::new(RwLock::new(get_current_timestamp_ms())), 25 | static_directory, 26 | is_shutdown_enabled, 27 | is_keepalive_enabled: false, 28 | is_rule_cache_enabled: false, 29 | rule_timeout_ms: timeout, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/bins/src/bin/datadog_static_analyzer_server/utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | /// gets the current timestamp 4 | pub fn get_current_timestamp_ms() -> u128 { 5 | SystemTime::now() 6 | .duration_since(UNIX_EPOCH) 7 | .unwrap() 8 | .as_millis() 9 | } 10 | 11 | /// gets the kernel version (CARGO) 12 | pub fn get_version() -> String { 13 | kernel::constants::CARGO_VERSION.to_string() 14 | } 15 | 16 | // gets the kernel revision (VERSION) 17 | pub fn get_revision() -> String { 18 | kernel::constants::VERSION.to_string() 19 | } 20 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli" 3 | edition = "2021" 4 | version.workspace = true 5 | 6 | [features] 7 | default = [] 8 | 9 | [dependencies] 10 | # local 11 | kernel = { package = "static-analysis-kernel", path = "../static-analysis-kernel" } 12 | secrets = { package = "secrets", path = "../secrets" } 13 | common = { package = "common", path = "../common" } 14 | # workspace 15 | anyhow = { workspace = true } 16 | base64 = { workspace = true } 17 | git2 = { workspace = true } 18 | serde = { workspace = true } 19 | serde_json = { workspace = true } 20 | derive_builder = { workspace = true } 21 | num_cpus = { workspace = true } 22 | serde-sarif = { workspace = true } 23 | sha2 = { workspace = true } 24 | thiserror = { workspace = true } 25 | uuid = { workspace = true } 26 | # other 27 | csv = "1.3.0" 28 | path-slash = "0.2.1" 29 | percent-encoding = "2.3.1" 30 | prettytable-rs = "0.10.0" 31 | # Default features are disabled to turn off "http2", "charset". 32 | reqwest = { version = "0.12.15", default-features = false, features = ["default-tls", "macos-system-configuration", "blocking", "json"] } 33 | valico = "4.0.0" 34 | walkdir = "2.5.0" 35 | 36 | [dev-dependencies] 37 | assert-json-diff = "2.0.2" 38 | tempfile = "3.20.0" 39 | -------------------------------------------------------------------------------- /crates/cli/resources/test/gitignore/test1: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | ddtrace/appsec/_ddwaf.cpp 8 | ddtrace/appsec/include 9 | ddtrace/appsec/share 10 | ddtrace/profiling/collector/_task.c 11 | ddtrace/profiling/_threading.c 12 | ddtrace/profiling/collector/_traceback.c 13 | ddtrace/profiling/collector/stack.c 14 | ddtrace/profiling/exporter/pprof.c 15 | ddtrace/profiling/_build.c 16 | ddtrace/internal/datadog/profiling/ddup.cpp 17 | ddtrace/internal/_encoding.c 18 | ddtrace/internal/_rand.c 19 | ddtrace/internal/_tagset.c 20 | *.so 21 | *.a 22 | 23 | # Cython annotate HTML files 24 | ddtrace/**/*.html 25 | 26 | # Dynamic binary libraries 27 | ddtrace/appsec/ddwaf/libddwaf/ 28 | libddwaf*.sha256 29 | ddtrace/internal/datadog/profiling/libdatadog/ 30 | 31 | # Distribution / packaging 32 | .Python 33 | env/ 34 | build/ 35 | develop-eggs/ 36 | dist/ 37 | downloads/ 38 | eggs/ 39 | .eggs/ 40 | lib64/ 41 | parts/ 42 | sdist/ 43 | var/ 44 | *.egg-info/ 45 | .installed.cfg 46 | *.egg 47 | *.whl 48 | 49 | # PyInstaller 50 | # Usually these files are written by a python script from a template 51 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 52 | *.manifest 53 | *.spec 54 | 55 | # Installer logs 56 | pip-log.txt 57 | pip-delete-this-directory.txt 58 | 59 | # Unit test / coverage reports 60 | htmlcov/ 61 | .tox/ 62 | .ddtox/ 63 | .ddriot/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | test-results/ 68 | coverage.xml 69 | coverage.json 70 | *,cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | test.db 74 | .benchmarks/**/*.json 75 | 76 | # Translations 77 | *.mo 78 | *.pot 79 | 80 | # Django stuff: 81 | *.log 82 | local_settings.py 83 | 84 | # Flask stuff: 85 | instance/ 86 | .webassets-cache 87 | 88 | # Scrapy stuff: 89 | .scrapy 90 | 91 | # Sphinx documentation 92 | docs/_build/ 93 | _readthedocs/ 94 | 95 | # PyBuilder 96 | target/ 97 | 98 | # IPython Notebook 99 | .ipynb_checkpoints 100 | 101 | # pyenv 102 | .python-version 103 | 104 | # celery beat schedule file 105 | celerybeat-schedule* 106 | 107 | # docker-compose env file 108 | # it must be versioned to keep track of backing services defaults 109 | !.env 110 | 111 | # virtualenv 112 | venv/ 113 | ENV/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # Vim 122 | *.swp 123 | # IDEA 124 | .idea/ 125 | 126 | # VS Code 127 | .vscode/ 128 | 129 | # Riot 130 | .riot/venv* 131 | .riot/requirements/*.in 132 | 133 | # Auto-generated version file 134 | ddtrace/_version.py 135 | 136 | # Benchmarks 137 | artifacts/ 138 | 139 | # IAST cpp 140 | ddtrace/appsec/iast/_taint_tracking/cmake-build-debug/* 141 | ddtrace/appsec/iast/_taint_tracking/_deps/* 142 | ddtrace/appsec/iast/_taint_tracking/CMakeFiles/* 143 | 144 | # CircleCI generated config 145 | .circleci/config.gen.yml 146 | -------------------------------------------------------------------------------- /crates/cli/resources/test/test_files_by_size/versions-empty.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/cli/resources/test/test_files_by_size/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0": { 3 | "0.0.9": { 4 | "cli": { 5 | "windows": { 6 | "x86_64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-x86_64-pc-windows-msvc.zip" 7 | }, 8 | "linux": { 9 | "x86_64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-x86_64-unknown-linux-gnu.zip", 10 | "aarch64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-aarch64-unknown-linux-gnu.zip" 11 | }, 12 | "macos": { 13 | "x86_64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-x86_64-apple-darwin.zip", 14 | "aarch64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-aarch64-apple-darwin.zip" 15 | } 16 | }, 17 | "server": { 18 | "windows": { 19 | "x86_64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-server-x86_64-pc-windows-msvc.zip" 20 | }, 21 | "linux": { 22 | "x86_64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-server-x86_64-unknown-linux-gnu.zip", 23 | "aarch64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-server-aarch64-unknown-linux-gnu.zip" 24 | }, 25 | "macos": { 26 | "x86_64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-server-x86_64-apple-darwin.zip", 27 | "aarch64": "https://github.com/DataDog/datadog-static-analyzer/releases/download/0.0.9/datadog-static-analyzer-server-aarch64-apple-darwin.zip" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/cli/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub static DATADOG_CONFIG_FILE_WITHOUT_PREFIX: &str = "static-analysis.datadog"; 2 | 3 | pub static DATADOG_HEADER_APP_KEY: &str = "dd-application-key"; 4 | pub static DATADOG_HEADER_API_KEY: &str = "dd-api-key"; 5 | pub static DATADOG_HEADER_JWT_TOKEN: &str = "dd-auth-jwt"; 6 | pub static HEADER_CONTENT_TYPE: &str = "Content-Type"; 7 | pub static HEADER_CONTENT_TYPE_APPLICATION_JSON: &str = "application/json"; 8 | pub static SARIF_PROPERTY_DATADOG_FINGERPRINT: &str = "DATADOG_FINGERPRINT"; 9 | pub static SARIF_PROPERTY_SHA: &str = "SHA"; 10 | pub static DEFAULT_MAX_CPUS: usize = 8; 11 | pub static DEFAULT_MAX_FILE_SIZE_KB: u64 = 200; 12 | // See https://docs.gitlab.com/ee/ci/variables/predefined_variables.html 13 | pub static GITLAB_ENVIRONMENT_VARIABLE_COMMIT_BRANCH: &str = "CI_COMMIT_BRANCH"; 14 | pub static GIT_HEAD: &str = "HEAD"; 15 | 16 | // application error: greater or equal to 10 and less than 50 17 | pub static EXIT_CODE_FAIL_ON_VIOLATION: i32 = 10; 18 | pub static EXIT_CODE_GITHOOK_FAILED: i32 = 11; 19 | pub static EXIT_CODE_RULE_CHECKSUM_INVALID: i32 = 12; 20 | 21 | // user errors, all more than 50 22 | pub static EXIT_CODE_INVALID_CONFIGURATION: i32 = 50; 23 | pub static EXIT_CODE_SHA_OR_DEFAULT_BRANCH: i32 = 51; 24 | pub static EXIT_CODE_NO_SECRET_OR_STATIC_ANALYSIS: i32 = 52; 25 | pub static EXIT_CODE_RULE_FILE_WITH_CONFIGURATION: i32 = 53; 26 | pub static EXIT_CODE_NO_OUTPUT: i32 = 54; 27 | pub static EXIT_CODE_NO_DIRECTORY: i32 = 55; 28 | pub static EXIT_CODE_INVALID_DIRECTORY: i32 = 56; 29 | pub static EXIT_CODE_UNSAFE_SUBDIRECTORIES: i32 = 57; 30 | pub static EXIT_CODE_RULESET_NOT_FOUND: i32 = 58; 31 | -------------------------------------------------------------------------------- /crates/cli/src/csv.rs: -------------------------------------------------------------------------------- 1 | use csv::Writer; 2 | use kernel::model::rule::{RuleCategory, RuleResult, RuleSeverity}; 3 | use secrets::model::secret_result::SecretResult; 4 | 5 | pub fn generate_csv_results( 6 | rule_results: &Vec, 7 | secrets_results: &[SecretResult], 8 | ) -> String { 9 | let mut wtr = Writer::from_writer(vec![]); 10 | wtr.write_record([ 11 | "filename", 12 | "rule", 13 | "category", 14 | "severity", 15 | "message", 16 | "start_line", 17 | "start_col", 18 | "end_line", 19 | "end_col", 20 | ]) 21 | .expect("csv serialization without issue"); 22 | 23 | for r in rule_results { 24 | for v in &r.violations { 25 | wtr.write_record(&[ 26 | r.filename.to_string(), 27 | r.rule_name.to_string(), 28 | v.category.to_string(), 29 | v.severity.to_string(), 30 | v.message.to_string(), 31 | v.start.line.to_string(), 32 | v.start.col.to_string(), 33 | v.end.line.to_string(), 34 | v.end.col.to_string(), 35 | ]) 36 | .expect("csv serialization without issue for violation"); 37 | } 38 | } 39 | 40 | for r in secrets_results { 41 | for v in &r.matches { 42 | wtr.write_record(&[ 43 | r.filename.to_string(), 44 | r.rule_name.to_string(), 45 | RuleCategory::Security.to_string(), 46 | RuleSeverity::Error.to_string(), 47 | r.message.to_string(), 48 | v.start.line.to_string(), 49 | v.start.col.to_string(), 50 | v.end.line.to_string(), 51 | v.end.col.to_string(), 52 | ]) 53 | .expect("csv serialization without issue for violation"); 54 | } 55 | } 56 | 57 | String::from_utf8(wtr.into_inner().expect("generate CSV file")).expect("generate CSV file") 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | 64 | use kernel::model::rule::{RuleCategory, RuleSeverity}; 65 | use kernel::model::violation::Violation; 66 | 67 | // execution time must be more than 0 68 | #[test] 69 | fn test_export_csv() { 70 | let res_no_result = generate_csv_results(&vec![], &vec![]); 71 | assert_eq!( 72 | res_no_result, 73 | "filename,rule,category,severity,message,start_line,start_col,end_line,end_col\n" 74 | ); 75 | let res_with_result = generate_csv_results( 76 | &vec![RuleResult { 77 | rule_name: "myrule".to_string(), 78 | filename: "filename".to_string(), 79 | violations: vec![Violation { 80 | start: common::model::position::Position { line: 10, col: 12 }, 81 | end: common::model::position::Position { line: 12, col: 10 }, 82 | message: "message".to_string(), 83 | severity: RuleSeverity::Error, 84 | category: RuleCategory::Performance, 85 | fixes: vec![], 86 | taint_flow: None, 87 | }], 88 | errors: vec![], 89 | execution_error: None, 90 | output: None, 91 | execution_time_ms: 10, 92 | query_node_time_ms: 0, 93 | parsing_time_ms: 0, 94 | }], 95 | &vec![], 96 | ); 97 | assert_eq!(res_with_result, "filename,rule,category,severity,message,start_line,start_col,end_line,end_col\nfilename,myrule,performance,error,message,10,12,12,10\n"); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config_file; 2 | pub mod constants; 3 | pub mod csv; 4 | pub mod datadog_utils; 5 | pub mod file_utils; 6 | pub mod git_utils; 7 | pub mod model; 8 | pub mod rule_utils; 9 | pub mod sarif; 10 | pub mod utils; 11 | pub mod violations_table; 12 | -------------------------------------------------------------------------------- /crates/cli/src/model.rs: -------------------------------------------------------------------------------- 1 | pub mod cli_configuration; 2 | pub mod datadog_api; 3 | -------------------------------------------------------------------------------- /crates/cli/src/sarif.rs: -------------------------------------------------------------------------------- 1 | pub mod sarif_utils; 2 | -------------------------------------------------------------------------------- /crates/cli/src/violations_table.rs: -------------------------------------------------------------------------------- 1 | use kernel::model::rule::RuleResult; 2 | use prettytable::{format, row, Table}; 3 | 4 | pub fn print_violations_table(rule_results: &[RuleResult]) { 5 | let mut table = Table::new(); 6 | let format = format::FormatBuilder::new() 7 | .separator( 8 | format::LinePosition::Title, 9 | format::LineSeparator::new('-', '-', '-', '-'), 10 | ) 11 | .padding(1, 1) 12 | .build(); 13 | table.set_format(format); 14 | table.set_titles(row![ 15 | "rule", "filename", "location", "category", "severity", "message" 16 | ]); 17 | for rule_result in rule_results { 18 | if !rule_result.violations.is_empty() { 19 | for violation in &rule_result.violations { 20 | let position = format!( 21 | "{}:{}-{}:{}", 22 | violation.start.line, 23 | violation.start.col, 24 | violation.end.line, 25 | violation.end.col 26 | ); 27 | table.add_row(row![ 28 | rule_result.rule_name, 29 | rule_result.filename, 30 | position, 31 | violation.category.to_string(), 32 | violation.severity.to_string(), 33 | violation.message 34 | ]); 35 | } 36 | } 37 | } 38 | table.printstd(); 39 | } 40 | -------------------------------------------------------------------------------- /crates/common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | edition = "2021" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | serde = { version = "1.0.219", features = ["derive"] } 9 | derive_builder = { workspace = true } 10 | 11 | # other 12 | bstr = "1.12.0" -------------------------------------------------------------------------------- /crates/common/src/analysis_options.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | // Used internally to pass options to the analysis 6 | #[derive(Clone, Deserialize, Debug, Serialize)] 7 | pub struct AnalysisOptions { 8 | pub log_output: bool, 9 | pub use_debug: bool, 10 | pub ignore_generated_files: bool, 11 | pub timeout: Option, 12 | } 13 | 14 | impl Default for AnalysisOptions { 15 | fn default() -> Self { 16 | Self { 17 | log_output: false, 18 | use_debug: false, 19 | ignore_generated_files: true, 20 | timeout: None, 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod analysis_options; 2 | pub mod model; 3 | pub mod utils; 4 | -------------------------------------------------------------------------------- /crates/common/src/model.rs: -------------------------------------------------------------------------------- 1 | pub mod diff_aware; 2 | pub mod position; 3 | -------------------------------------------------------------------------------- /crates/common/src/model/diff_aware.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /// This trait defines what is required for diff-aware to run correctly. Diff-aware needs to 6 | /// be activated for a certain configuration. All elements that makes the configuration 7 | /// or the results change should implement this trait and generate a unique string 8 | /// that is used to produce a configuration hash. 9 | pub trait DiffAware { 10 | /// The string that generates a string used to produce a unique hash for the configuration. 11 | /// This string should always be the same at each run if the configuration did not change. 12 | fn generate_diff_aware_digest(&self) -> String; 13 | } 14 | -------------------------------------------------------------------------------- /crates/common/src/model/position.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use derive_builder::Builder; 6 | use serde::{Deserialize, Serialize}; 7 | use std::fmt; 8 | 9 | #[derive(Deserialize, Debug, Serialize, Clone, Copy, Builder, PartialEq, Eq, Hash)] 10 | pub struct Position { 11 | pub line: u32, 12 | pub col: u32, 13 | } 14 | 15 | impl Position { 16 | pub fn new(line: u32, col: u32) -> Self { 17 | Self { line, col } 18 | } 19 | } 20 | 21 | impl fmt::Display for Position { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | write!(f, "position (line: {}, col: {})", self.line, self.col) 24 | } 25 | } 26 | 27 | /// A contiguous portion of a file. 28 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] 29 | pub struct Region { 30 | /// The start of a region. 31 | /// * `line`: A positive integer equal to the line number containing the first character of this region. 32 | /// * `col`: A positive integer equal to the column number of the first character of this region. 33 | pub start: Position, 34 | /// The end of a region. 35 | /// * `line`: A positive integer equal to the line number containing the last character of this region. 36 | /// * `col`: A positive integer whose value is one greater than column number of the last character in this region. 37 | pub end: Position, 38 | } 39 | -------------------------------------------------------------------------------- /crates/common/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod position_utils; 2 | -------------------------------------------------------------------------------- /crates/secrets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secrets" 3 | edition = "2021" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | # workspace 8 | anyhow = { workspace = true } 9 | common = { package = "common", path = "../common" } 10 | itertools = { workspace = true } 11 | serde = { workspace = true } 12 | serde_json = { workspace = true } 13 | 14 | futures = "0.3.31" 15 | lazy_static = "1.5.0" 16 | 17 | # remote 18 | dd-sds = { git = "https://github.com/DataDog/dd-sensitive-data-scanner.git", rev = "95f3884aa7a502b848d234c27cc53800ad2a0947" } 19 | -------------------------------------------------------------------------------- /crates/secrets/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod model; 2 | pub mod scanner; 3 | pub mod secret_files; 4 | -------------------------------------------------------------------------------- /crates/secrets/src/model.rs: -------------------------------------------------------------------------------- 1 | pub mod secret_result; 2 | pub mod secret_rule; 3 | -------------------------------------------------------------------------------- /crates/secrets/src/model/secret_result.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use common::model::position::Position; 6 | use dd_sds::MatchStatus; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Serialize, Deserialize)] 10 | pub enum SecretValidationStatus { 11 | NotValidated, 12 | Valid, 13 | Invalid, 14 | ValidationError, 15 | NotAvailable, 16 | } 17 | 18 | impl From<&MatchStatus> for SecretValidationStatus { 19 | fn from(value: &MatchStatus) -> Self { 20 | match value { 21 | MatchStatus::NotChecked => SecretValidationStatus::NotValidated, 22 | MatchStatus::Valid => SecretValidationStatus::Valid, 23 | MatchStatus::Invalid => SecretValidationStatus::Invalid, 24 | MatchStatus::Error(_) => SecretValidationStatus::ValidationError, 25 | MatchStatus::NotAvailable => SecretValidationStatus::NotAvailable, 26 | } 27 | } 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash, Eq)] 31 | pub struct SecretResultMatch { 32 | pub start: Position, 33 | pub end: Position, 34 | pub validation_status: SecretValidationStatus, 35 | } 36 | 37 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash, Eq)] 38 | pub struct SecretResult { 39 | pub rule_id: String, 40 | pub rule_name: String, 41 | pub filename: String, 42 | pub message: String, 43 | pub matches: Vec, 44 | } 45 | -------------------------------------------------------------------------------- /crates/secrets/src/secret_files.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::path::Path; 3 | 4 | lazy_static! { 5 | static ref IGNORE_FILENAMES: Vec<&'static str> = vec![ 6 | "go.mod", 7 | "WORKSPACE", 8 | "Gopkg.lock", 9 | "package-lock.json", 10 | "yarn.lock", 11 | "repositories.bzl", 12 | "mirror.cfg", 13 | ]; 14 | static ref IGNORE_SUFFIXES: Vec<&'static str> = 15 | vec!["ico", "jpeg", "jpg", "png", "tif", "tiff", "gif"]; 16 | } 17 | 18 | /// Return true if the file should be ignored, false otherwise. 19 | /// This function is only valid for secrets. 20 | pub fn should_ignore_file_for_secret(path: &Path) -> bool { 21 | if let Some(ext) = path.extension() { 22 | if let Some(e) = ext.to_str() { 23 | if IGNORE_SUFFIXES.contains(&e) { 24 | return true; 25 | } 26 | } 27 | } 28 | if let Some(filename) = path.file_name() { 29 | if let Some(f) = filename.to_str() { 30 | if IGNORE_FILENAMES.contains(&f) { 31 | return true; 32 | } 33 | } 34 | } 35 | false 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | use std::path::PathBuf; 42 | 43 | #[test] 44 | fn test_should_ignore_file_for_secret() { 45 | assert!(should_ignore_file_for_secret(&PathBuf::from( 46 | "/path/to/file.gif" 47 | ))); 48 | assert!(!should_ignore_file_for_secret(&PathBuf::from( 49 | "/path/to/file.py" 50 | ))); 51 | assert!(should_ignore_file_for_secret(&PathBuf::from( 52 | "/path/to/go.mod" 53 | ))); 54 | assert!(should_ignore_file_for_secret(&PathBuf::from( 55 | "/path/to/repositories.bzl" 56 | ))); 57 | assert!(should_ignore_file_for_secret(&PathBuf::from( 58 | "/path/to/yarn.lock" 59 | ))); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static-analysis-kernel" 3 | edition = "2021" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | # workspace 8 | anyhow = { workspace = true } 9 | base64 = { workspace = true } 10 | common = { package = "common", path = "../common" } 11 | serde = { workspace = true } 12 | serde_json = { workspace = true } 13 | derive_builder = { workspace = true } 14 | serde-sarif = { workspace = true } 15 | sha2 = { workspace = true } 16 | indexmap = { workspace = true } 17 | thiserror = { workspace = true } 18 | tree-sitter = { workspace = true } 19 | 20 | # other 21 | deno_core = "0.330.0" 22 | globset = "0.4.16" 23 | graphviz-rust = "0.9.4" 24 | sequence_trie = "0.3.6" 25 | serde_yaml = "0.9.21" 26 | streaming-iterator = "0.1.9" 27 | 28 | # We're experiencing issues with v8 130.0.8. Until we can resolve this, pin to the last-known-working. 29 | v8 = "=130.0.7" 30 | 31 | [build-dependencies] 32 | cc = "1.2.21" 33 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis.rs: -------------------------------------------------------------------------------- 1 | pub mod analyze; 2 | pub mod ddsa_lib; 3 | pub mod generated_content; 4 | pub mod languages; 5 | pub mod tree_sitter; 6 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | pub mod bridge; 6 | pub mod common; 7 | pub mod context; 8 | pub use context::*; 9 | pub mod extension; 10 | pub(crate) mod js; 11 | pub(crate) mod ops; 12 | pub(crate) mod resource_watchdog; 13 | pub(crate) mod runtime; 14 | pub use runtime::JsRuntime; 15 | #[allow(dead_code)] 16 | pub mod test_utils; 17 | pub mod v8_ds; 18 | pub mod v8_platform; 19 | 20 | use std::hash::{Hash, Hasher}; 21 | 22 | #[derive(Debug, Copy, Clone)] 23 | pub struct RawTSNode(tree_sitter::ffi::TSNode); 24 | 25 | impl RawTSNode { 26 | pub fn new(node: tree_sitter::Node) -> Self { 27 | Self(node.into_raw()) 28 | } 29 | 30 | /// Constructs a [`tree_sitter::Node`] from the raw tree-sitter node. 31 | /// 32 | /// # Safety 33 | /// The caller must ensure the that the [`ffi::Tree`](tree_sitter::ffi::Tree) this node comes from 34 | /// has not been dropped. 35 | #[allow(clippy::wrong_self_convention)] 36 | // (This clippy lint is overridden in order to tie the `tree_sitter::Node<'a>` lifetime 37 | // to `&'a RawTSNode` to make it harder to do something unintended. Note that this restriction 38 | // is artificial because `RawTSNode` is Copy). 39 | unsafe fn to_node(&self) -> tree_sitter::Node { 40 | tree_sitter::Node::from_raw(self.0) 41 | } 42 | } 43 | 44 | impl From> for RawTSNode { 45 | fn from(value: tree_sitter::Node) -> Self { 46 | Self::new(value) 47 | } 48 | } 49 | 50 | impl Hash for RawTSNode { 51 | fn hash(&self, state: &mut H) { 52 | let ctx = &self.0.context; 53 | state.write_usize(ctx[0] as usize); 54 | state.write_usize(ctx[1] as usize); 55 | state.write_usize(ctx[2] as usize); 56 | state.write_usize(ctx[3] as usize); 57 | state.write_usize(self.0.id as usize); 58 | state.write_usize(self.0.tree as usize); 59 | } 60 | } 61 | 62 | impl Eq for RawTSNode {} 63 | impl PartialEq for RawTSNode { 64 | fn eq(&self, other: &Self) -> bool { 65 | self.0.id == other.0.id && self.0.context == other.0.context && self.0.tree == other.0.tree 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/bridge.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod context; 6 | pub use context::ContextBridge; 7 | mod query_match; 8 | pub use query_match::QueryMatchBridge; 9 | mod ts_node; 10 | pub use ts_node::TsNodeBridge; 11 | mod violation; 12 | pub use violation::ViolationBridge; 13 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/context.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod file; 6 | pub use file::FileContext; 7 | pub(crate) mod file_go; 8 | pub use file_go::FileContextGo; 9 | pub(crate) mod file_tf; 10 | pub use file_tf::FileContextTerraform; 11 | pub(crate) mod file_js; 12 | pub use file_js::FileContextJavaScript; 13 | mod root; 14 | pub use root::RootContext; 15 | mod rule; 16 | pub use rule::RuleContext; 17 | pub(crate) mod ts_lang; 18 | pub use ts_lang::TsLanguageContext; 19 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/context/file.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use super::file_js::FileContextJavaScript; 6 | use super::FileContextTerraform; 7 | use crate::analysis::ddsa_lib::context::file_go::FileContextGo; 8 | 9 | #[derive(Debug, Default)] 10 | pub struct FileContext { 11 | // Supported file contexts: 12 | go: Option, 13 | terraform: Option, 14 | javascript: Option, 15 | } 16 | 17 | impl FileContext { 18 | // Returns a mutable reference to the [`FileContextGo`] owned by this `FileContext`, if it exists. 19 | pub fn go_mut(&mut self) -> Option<&mut FileContextGo> { 20 | self.go.as_mut() 21 | } 22 | 23 | // Returns a reference to the [`FileContextGo`] owned by this `FileContext`, if it exists. 24 | pub fn go(&self) -> Option<&FileContextGo> { 25 | self.go.as_ref() 26 | } 27 | 28 | // Assigns the [`FileContextGo`] to this `FileContext`, returning the old value, if it exists. 29 | pub fn set_go(&mut self, file_ctx_go: FileContextGo) -> Option { 30 | Option::replace(&mut self.go, file_ctx_go) 31 | } 32 | 33 | /// Returns a mutable reference to the [`FileContextTerraform`] owned by this `FileContext`, if it exists. 34 | pub fn tf_mut(&mut self) -> Option<&mut FileContextTerraform> { 35 | self.terraform.as_mut() 36 | } 37 | 38 | /// Returns a reference to the [`FileContextTerraform`] owned by this `FileContext`, if it exists. 39 | pub fn tf(&self) -> Option<&FileContextTerraform> { 40 | self.terraform.as_ref() 41 | } 42 | 43 | /// Assigns the [`FileContextTerraform`] to this `FileContext`, returning the old value, if it exists. 44 | pub fn set_tf(&mut self, file_ctx_tf: FileContextTerraform) -> Option { 45 | self.terraform.replace(file_ctx_tf) 46 | } 47 | 48 | /// Returns a mutable reference to the [`FileContextJavaScript`] owned by this `FileContext`, if it exists. 49 | pub fn js_mut(&mut self) -> Option<&mut FileContextJavaScript> { 50 | self.javascript.as_mut() 51 | } 52 | 53 | /// Returns a reference to the [`FileContextJavaScript`] owned by this `FileContext`, if it exists. 54 | pub fn js(&self) -> Option<&FileContextJavaScript> { 55 | self.javascript.as_ref() 56 | } 57 | 58 | /// Assigns the [`FileContextJavaScript`] to this `FileContext`, returning the old value, if it exists. 59 | pub fn set_js(&mut self, file_ctx_js: FileContextJavaScript) -> Option { 60 | self.javascript.replace(file_ctx_js) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/context/file_js.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use deno_core::v8; 6 | use deno_core::v8::HandleScope; 7 | use std::borrow::Cow; 8 | 9 | use crate::analysis::ddsa_lib::common::{Class, DDSAJsRuntimeError}; 10 | use crate::analysis::ddsa_lib::js::JSPackageImport; 11 | use crate::analysis::ddsa_lib::v8_ds::MirroredVec; 12 | use crate::analysis::languages; 13 | 14 | /// Structure for the file context that is specific to JavaScript. 15 | #[derive(Debug)] 16 | pub struct FileContextJavaScript { 17 | imports: MirroredVec>, 18 | } 19 | 20 | /// A duplicate of [`languages::javascript::PackageImport`](crate::analysis::languages::javascript::PackageImport), 21 | /// except that this struct is guaranteed to own its data. 22 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 23 | pub struct PackageImport { 24 | pub name: String, 25 | pub imported_from: Option, 26 | } 27 | 28 | impl FileContextJavaScript { 29 | pub fn new(scope: &mut HandleScope) -> Result { 30 | let imports = MirroredVec::new(JSPackageImport::try_new(scope)?, scope); 31 | Ok(Self { imports }) 32 | } 33 | 34 | pub fn update_state(&mut self, scope: &mut HandleScope, tree: &tree_sitter::Tree, code: &str) { 35 | let imports = languages::javascript::parse_imports_with_tree(code, tree); 36 | let owned_imports = imports 37 | .into_iter() 38 | .map(|pkg| PackageImport { 39 | name: pkg.name.into_owned(), 40 | imported_from: pkg.imported_from.map(Cow::into_owned), 41 | }) 42 | .collect::>(); 43 | 44 | self.imports.set_data(scope, owned_imports); 45 | } 46 | 47 | pub fn clear(&mut self, scope: &mut HandleScope) { 48 | self.imports.clear(scope); 49 | } 50 | 51 | /// Returns a reference to the [`v8::Global`] array backing the imports. 52 | pub(crate) fn imports_v8_array(&self) -> &v8::Global { 53 | self.imports.v8_array() 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use super::*; 60 | use crate::analysis::ddsa_lib::test_utils::cfg_test_v8; 61 | use crate::analysis::tree_sitter::get_tree; 62 | use crate::model::common::Language; 63 | 64 | /// Tests that imports are properly exposed via the context. 65 | #[test] 66 | fn test_get_js_imports() { 67 | let mut runtime = cfg_test_v8().deno_core_rt(); 68 | let scope = &mut runtime.handle_scope(); 69 | let mut ctx_js = FileContextJavaScript::new(scope).unwrap(); 70 | 71 | let code_1 = "\ 72 | import { foo } from './module1/file1.js'; 73 | import { bar } from './module1/file2.js'; 74 | "; 75 | let expected_1 = vec![("foo", "file1"), ("bar", "file2")] 76 | .into_iter() 77 | .map(|(name, imported_from)| PackageImport { 78 | name: name.to_string(), 79 | imported_from: Some(imported_from.to_string()), 80 | }) 81 | .collect::>(); 82 | let tree_1 = get_tree(code_1, &Language::JavaScript).unwrap(); 83 | ctx_js.update_state(scope, &tree_1, code_1); 84 | assert_eq!(ctx_js.imports.get(0).unwrap(), &expected_1[0]); 85 | assert_eq!(ctx_js.imports.get(1).unwrap(), &expected_1[1]); 86 | 87 | // Additionally, test that multiple calls to `update_state` properly clear state. 88 | let code_2 = "\ 89 | import { baz } from './module1/file3.js'; 90 | "; 91 | let expected_2 = vec![PackageImport { 92 | name: "baz".to_string(), 93 | imported_from: Some("file3".to_string()), 94 | }]; 95 | let tree_2 = get_tree(code_2, &Language::JavaScript).unwrap(); 96 | ctx_js.update_state(scope, &tree_2, code_2); 97 | assert_eq!(ctx_js.imports.get(0).unwrap(), &expected_2[0]); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/context/rule.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use crate::analysis::ddsa_lib::common::v8_string; 6 | use crate::analysis::ddsa_lib::v8_ds::MirroredIndexMap; 7 | use deno_core::v8; 8 | use deno_core::v8::HandleScope; 9 | 10 | /// A stateful struct containing metadata related to a rule. 11 | #[derive(Debug)] 12 | pub struct RuleContext { 13 | /// A mapping from argument names to values 14 | arguments: MirroredIndexMap, 15 | } 16 | 17 | impl RuleContext { 18 | pub fn new(scope: &mut HandleScope) -> Self { 19 | let arguments = MirroredIndexMap::::new(scope); 20 | RuleContext { arguments } 21 | } 22 | 23 | /// Inserts an argument name and value pair 24 | pub fn insert_argument( 25 | &mut self, 26 | scope: &mut HandleScope, 27 | name: impl Into, 28 | value: impl Into, 29 | ) { 30 | let name = name.into(); 31 | let value = value.into(); 32 | self.arguments 33 | .insert_with(scope, name, value, |scope, key, value| { 34 | let key = v8_string(scope, key); 35 | let value = v8_string(scope, value); 36 | (key.into(), value.into()) 37 | }); 38 | } 39 | 40 | /// Clears all arguments from the map, preserving the original allocation across Rust and v8. 41 | pub fn clear_arguments(&mut self, scope: &mut HandleScope) { 42 | self.arguments.clear(scope); 43 | } 44 | 45 | /// Returns a reference to the [`v8::Global`] arguments map 46 | pub fn arguments_map(&self) -> &v8::Global { 47 | self.arguments.v8_map() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/extension.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use crate::analysis::ddsa_lib::ops; 6 | 7 | deno_core::extension!( 8 | ddsa_lib, 9 | ops = [ 10 | ops::op_current_filename, 11 | ops::op_console_push, 12 | ops::op_current_ts_tree_text, 13 | ops::op_ts_node_named_children, 14 | ops::op_ts_node_parent, 15 | ops::op_ts_node_text, 16 | // Language-specific 17 | ops::op_java_get_bin_expr_operator, 18 | ops::op_digraph_adjacency_list_to_dot, 19 | ], 20 | esm_entry_point = "ext:ddsa_lib/__bootstrap.js", 21 | esm = [ 22 | dir "src/analysis/ddsa_lib/js", 23 | "__bootstrap.js", 24 | "ext:ddsa_lib/context_file" = "context_file.js", 25 | "ext:ddsa_lib/context_file_go" = "context_file_go.js", 26 | "ext:ddsa_lib/context_file_js" = "context_file_js.js", 27 | "ext:ddsa_lib/context_file_tf" = "context_file_tf.js", 28 | "ext:ddsa_lib/context_root" = "context_root.js", 29 | "ext:ddsa_lib/context_rule" = "context_rule.js", 30 | "ext:ddsa_lib/context_ts_lang" = "context_ts_lang.js", 31 | "ext:ddsa_lib/ddsa" = "ddsa.js", 32 | "ext:ddsa_lib/edit" = "edit.js", 33 | "ext:ddsa_lib/fix" = "fix.js", 34 | "ext:ddsa_lib/flow/graph" = "flow/graph.js", 35 | "ext:ddsa_lib/flow/java" = "flow/java.js", 36 | "ext:ddsa_lib/query_match" = "query_match.js", 37 | "ext:ddsa_lib/query_match_compat" = "query_match_compat.js", 38 | "ext:ddsa_lib/region" = "region.js", 39 | "ext:ddsa_lib/stella_compat" = "stella_compat.js", 40 | "ext:ddsa_lib/utility" = "utility.js", 41 | "ext:ddsa_lib/ts_node" = "ts_node.js", 42 | "ext:ddsa_lib/violation" = "violation.js", 43 | ], 44 | ); 45 | 46 | #[cfg(test)] 47 | deno_core::extension!( 48 | ddsa_lib_cfg_test, 49 | ops = [ops::cfg_test_op_rust_option], 50 | esm = [ 51 | dir "src/analysis/ddsa_lib/js", 52 | "ext:ddsa_lib_cfg_test/helpers" = "test_helpers.js", 53 | ] 54 | ); 55 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | // These lints are temporarily disabled while transitioning to ddsa_lib. 6 | #![allow(unused_imports, dead_code)] 7 | 8 | mod capture; 9 | pub(crate) use capture::*; 10 | mod context_file; 11 | pub(crate) use context_file::FileContext; 12 | mod context_file_go; 13 | pub(crate) use context_file_go::FileContextGo; 14 | mod context_file_js; 15 | pub(crate) use context_file_js::*; 16 | mod context_file_tf; 17 | pub(crate) use context_file_tf::*; 18 | mod context_root; 19 | pub(crate) use context_root::RootContext; 20 | mod context_rule; 21 | pub(crate) use context_rule::RuleContext; 22 | mod context_ts_lang; 23 | pub(crate) use context_ts_lang::TsLanguageContext; 24 | mod ddsa; 25 | mod edit; 26 | pub(crate) use edit::*; 27 | mod fix; 28 | pub(crate) use fix::*; 29 | pub(crate) mod flow; 30 | mod query_match; 31 | pub(crate) use query_match::*; 32 | mod query_match_compat; 33 | pub(crate) use query_match_compat::*; 34 | mod region; 35 | pub(crate) use region::CodeRegion; 36 | mod ts_node; 37 | pub(crate) use ts_node::*; 38 | mod stella_compat; 39 | pub(crate) use stella_compat::*; 40 | mod utility; 41 | mod violation; 42 | pub(crate) use violation::*; 43 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/__bootstrap.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | "use strict"; 6 | 7 | import {CodeRegion} from "ext:ddsa_lib/region"; 8 | import {DDSA, DDSAPrivate} from "ext:ddsa_lib/ddsa"; 9 | import {DDSA_Console} from "ext:ddsa_lib/utility"; 10 | import {Digraph} from "ext:ddsa_lib/flow/graph"; 11 | import {FileContext} from "ext:ddsa_lib/context_file"; 12 | import {FileContextGo} from "ext:ddsa_lib/context_file_go"; 13 | import {FileContextTerraform, TerraformResource} from "ext:ddsa_lib/context_file_tf"; 14 | import {FileContextJavaScript, PackageImport} from "ext:ddsa_lib/context_file_js"; 15 | import {Fix} from "ext:ddsa_lib/fix"; 16 | import {QueryMatch} from "ext:ddsa_lib/query_match"; 17 | import {QueryMatchCompat} from "ext:ddsa_lib/query_match_compat"; 18 | import {RootContext} from "ext:ddsa_lib/context_root"; 19 | import {RuleContext} from "ext:ddsa_lib/context_rule"; 20 | import {TreeSitterFieldChildNode, TreeSitterNode} from "ext:ddsa_lib/ts_node"; 21 | import {TsLanguageContext} from "ext:ddsa_lib/context_ts_lang"; 22 | import {Violation} from "ext:ddsa_lib/violation"; 23 | // TODO(JF): These are only used by the Rust runtime, which currently expects them in global scope, but 24 | // these should be hidden inside another object, not `globalThis`. 25 | globalThis.CodeRegion = CodeRegion; 26 | globalThis.DDSA_Console = DDSA_Console; 27 | globalThis.DDSA = DDSA; 28 | globalThis.Digraph = Digraph; 29 | globalThis.FileContext = FileContext; 30 | globalThis.FileContextGo = FileContextGo; 31 | globalThis.FileContextJavaScript = FileContextJavaScript; 32 | globalThis.PackageImport = PackageImport; 33 | globalThis.FileContextTerraform = FileContextTerraform; 34 | globalThis.Fix = Fix; 35 | globalThis.TerraformResource = TerraformResource; 36 | globalThis.QueryMatch = QueryMatch; 37 | globalThis.QueryMatchCompat = QueryMatchCompat; 38 | globalThis.RootContext = RootContext; 39 | globalThis.RuleContext = RuleContext; 40 | globalThis.TreeSitterNode = TreeSitterNode; 41 | globalThis.TreeSitterFieldChildNode = TreeSitterFieldChildNode; 42 | globalThis.TsLanguageContext = TsLanguageContext; 43 | globalThis.Violation = Violation; 44 | 45 | /////////// 46 | // Take all exports from `stella_compat.js` and define them within this scope. 47 | // NOTE: This is temporary scaffolding used during the transition to `ddsa_lib::JsRuntime`. 48 | import * as stellaCompat from "ext:ddsa_lib/stella_compat"; 49 | for (const [name, obj] of Object.entries(stellaCompat)) { 50 | globalThis[name] = obj; 51 | } 52 | /////////// 53 | 54 | globalThis.console = new DDSA_Console(); 55 | globalThis.ddsa = new DDSA(); 56 | // Note: The name "private" is just used to communicate intent -- there is no enforcement preventing rules from using this. 57 | globalThis.__ddsaPrivate__ = new DDSAPrivate(); 58 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/capture.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * @typedef {SingleCapture | MultiCapture} NamedCapture 7 | * Note that both interfaces use `string` for `name` because these objects are only created directly 8 | * via the v8 API, so we can guarantee that these will be implemented as interned strings. 9 | */ 10 | 11 | /** 12 | * @typedef {Object} SingleCapture 13 | * @property {string} name The name of the capture. 14 | * @property {NodeId} nodeId The node that was captured. 15 | * 16 | * Example tree-sitter query: 17 | * ``` 18 | * (identifier) @capture_name 19 | * ``` 20 | */ 21 | 22 | /** 23 | * @typedef {Object} MultiCapture 24 | * @property {string} name The name of the capture. 25 | * @property {Array} nodeIds The nodes that were captured. 26 | * 27 | * Example tree-sitter query: 28 | * ``` 29 | * (identifier) @duplicate_name 30 | * (number) @duplicate_name 31 | * ``` 32 | */ 33 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/context_file.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * Metadata related to a specific filetype. 7 | */ 8 | export class FileContext { 9 | /** 10 | * Creates a new, empty `FileContext`. 11 | */ 12 | constructor() { 13 | /** 14 | * A `go` file context. 15 | * @type {FileContextGo | undefined} 16 | */ 17 | this.go = undefined; 18 | /** 19 | * A `terraform` file context. 20 | * @type {FileContextTerraform | undefined} 21 | */ 22 | this.terraform = undefined; 23 | /** 24 | * A `javascript` file context. 25 | * @type {FileContextJavascript | undefined} 26 | */ 27 | this.javascript = undefined; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/context_file_go.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | import {SEALED_EMPTY_ARRAY} from "ext:ddsa_lib/utility"; 6 | 7 | /** 8 | * A fully-qualified package name. 9 | * @example 10 | * For the given import: 11 | * ``` 12 | * import ( 13 | * procstatsd "github.com/DataDog/datadog-agent/pkg/process/statsd" 14 | * ) 15 | * ``` 16 | * the fully-qualified package name is `github.com/DataDog/datadog-agent/pkg/process/statsd`. 17 | * @typedef {string} PackageFullyQualifiedName 18 | */ 19 | 20 | /** 21 | * An alias for an imported package. 22 | * @example 23 | * For the given import: 24 | * ``` 25 | * import ( 26 | * procstatsd "github.com/DataDog/datadog-agent/pkg/process/statsd" 27 | * ) 28 | * ``` 29 | * the alias is `procstatsd`. 30 | * @typedef {string} PackageAlias 31 | */ 32 | 33 | /** 34 | * Metadata about a `go` file. 35 | */ 36 | export class FileContextGo { 37 | /** 38 | * Creates a new `FileContextGo`. 39 | * @param {Map | undefined} aliasMap 40 | */ 41 | constructor(aliasMap) { 42 | /** 43 | * A map from a package alias to its fully-qualified name. 44 | * @type {Map | undefined} 45 | * */ 46 | this.aliasMap = aliasMap; 47 | } 48 | 49 | /** 50 | * Returns the fully-qualified name of a package, given an input alias. 51 | * @param {string} alias 52 | * @returns {string | undefined} 53 | */ 54 | getResolvedPackage(alias) { 55 | return this.aliasMap?.get(alias); 56 | } 57 | 58 | /** 59 | * Returns an array of fully qualified package names in an arbitrary order. 60 | * @returns {Array} 61 | */ 62 | get packages() { 63 | // For implementation simplicity, we are de-duplicating the FQ package names by 64 | // allocating a Set on every call and then returning it. Should this become a performance 65 | // bottleneck, this will be refactored so the Set is pre-generated/cached. 66 | if (this.aliasMap === undefined) { 67 | return SEALED_EMPTY_ARRAY; 68 | } else { 69 | const uniquePackages = new Set(this.aliasMap.values()); 70 | return Array.from(uniquePackages); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/context_file_js.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | export class FileContextJavaScript { 6 | /** 7 | * Creates a new `FileContextJavaScript`. 8 | * @param {PackageImport[]} packageImports 9 | */ 10 | constructor(packageImports) { 11 | /** 12 | * The imports in the current file. 13 | * @type {PackageImport[]} 14 | */ 15 | this.imports = packageImports; 16 | } 17 | 18 | /** 19 | * Returns whether the given package is imported in the current file. 20 | * 21 | * @param {string} packageName 22 | * 23 | * @returns {boolean} 24 | */ 25 | importsPackage(packageName) { 26 | return this.imports.some((i) => { 27 | if (i.isModule()) { 28 | return i.name === packageName; 29 | } 30 | return i.importedFrom === packageName 31 | }); 32 | } 33 | } 34 | 35 | /** 36 | * @private 37 | */ 38 | export class PackageImport { 39 | /** 40 | * Creates a new `PackageImport`. 41 | * 42 | * @param {string} name 43 | * @param {string | undefined} importedFrom 44 | */ 45 | constructor(name, importedFrom) { 46 | /** 47 | * The name of the item being imported. 48 | * @type {string} 49 | */ 50 | this.name = name; 51 | /** 52 | * The package that the item is being imported from. Note that this will be `undefined` if we are 53 | * importing the module as a whole, instead of a specific item from the module. 54 | * @type {string | undefined} 55 | */ 56 | this.importedFrom = importedFrom; 57 | } 58 | 59 | /** 60 | * Returns whether this import is a module or not. 61 | * 62 | * @returns {boolean} 63 | */ 64 | isModule() { 65 | return this.importedFrom === undefined; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/context_file_tf.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * An object representing a resource within a `Terraform` file. 7 | * @typedef {Object} Resource 8 | * @property {string} type 9 | * @property {string} name 10 | */ 11 | 12 | export class FileContextTerraform { 13 | /** 14 | * Creates a new `FileContextTerraform`. 15 | * @param {Resource[]} resourceList 16 | */ 17 | constructor(resourceList) { 18 | /** 19 | * A list of resources within the file. 20 | * @type {Resource[]} 21 | * */ 22 | this.resources = resourceList; 23 | } 24 | 25 | /** 26 | * Returns whether the file has a resource with the given type and name. 27 | * @param {string} type 28 | * @param {string} name 29 | * @returns {boolean} 30 | */ 31 | hasResource(type, name) { 32 | return this.resources.some(resource => resource.type === type && resource.name === name); 33 | } 34 | 35 | /** 36 | * Returns the resources with the given type. 37 | * @param {string} type 38 | * @returns {Resource[]} 39 | */ 40 | getResourcesOfType(type) { 41 | return this.resources.filter(resource => resource.type === type); 42 | } 43 | } 44 | 45 | export class TerraformResource { 46 | /** 47 | * @param {string} type 48 | * @param {string} name 49 | */ 50 | constructor(type, name) { 51 | /** 52 | * The type of the resource. 53 | * @type {string} 54 | */ 55 | this.type = type; 56 | 57 | /** 58 | * The name of the resource. 59 | * @type {string} 60 | */ 61 | this.name = name; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/context_root.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | const { 6 | op_current_ts_tree_text, 7 | op_current_filename, 8 | } = Deno.core.ops; 9 | 10 | /** 11 | * Metadata related the execution of a JavaScript rule. 12 | */ 13 | export class RootContext { 14 | /** 15 | * @param {FileContext} fileCtx 16 | * @param {RuleContext} ruleCtx 17 | * @param {TsLanguageContext} tsLangCtx 18 | */ 19 | constructor(fileCtx, ruleCtx, tsLangCtx) { 20 | /** 21 | * The filename of the file being analyzed within this context. This is intended to be an internal 22 | * field. External callers should use the {@link RootContext.filename} getter. 23 | * @type {string | undefined} 24 | * @private 25 | */ 26 | this.__js_cachedFilename = undefined; 27 | /** 28 | * The contents of the file. This is intended to be an internal field. External callers 29 | * should use the {@link RootContext.fileContents} getter. 30 | * @type {string | undefined} 31 | * @private 32 | */ 33 | this.__js_cachedFileContents = undefined; 34 | /** 35 | * An object that provides extra metadata for a specific filetypes. 36 | * @type {FileContext} 37 | * @private 38 | */ 39 | this.fileCtx = fileCtx; 40 | /** 41 | * An object that provides extra metadata specific to a rule. 42 | * @type {RuleContext} 43 | * @private 44 | */ 45 | this.ruleCtx = ruleCtx; 46 | /** 47 | * An object that provides metadata specific to a tree-sitter Language. 48 | * @type {TsLanguageContext} 49 | * @private 50 | */ 51 | this.tsLangCtx = tsLangCtx; 52 | } 53 | 54 | /** 55 | * A getter for the contents of the entire file. 56 | * @returns {string} 57 | */ 58 | get fileContents() { 59 | if (this.__js_cachedFileContents === undefined) { 60 | this.__js_cachedFileContents = op_current_ts_tree_text(); 61 | } 62 | return this.__js_cachedFileContents; 63 | } 64 | 65 | /** 66 | * A getter for the name of the file. 67 | * 68 | * @remarks 69 | * This lazily makes a call to Rust to retrieve the filename. Subsequent calls to this getter will 70 | * return the cached value. 71 | * 72 | * @returns {string} 73 | */ 74 | get filename() { 75 | if (this.__js_cachedFilename === undefined) { 76 | this.__js_cachedFilename = op_current_filename(); 77 | } 78 | return this.__js_cachedFilename; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/context_rule.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * A context related to the rule that is currently executing. 7 | */ 8 | export class RuleContext { 9 | /** 10 | * @param {Map | undefined} args 11 | */ 12 | constructor(args) { 13 | /** 14 | * A `Map` from the argument name to its string value. 15 | * @type { Map | undefined} 16 | * @private 17 | */ 18 | this._arguments = args; 19 | } 20 | 21 | /** 22 | * Gets the value of the argument with the given name, if it exists. 23 | * @param {string} name 24 | * @return {string | undefined} 25 | * 26 | * Arguments are defined by the Rust static analysis kernel, and come from user-defined rule overrides. 27 | */ 28 | getArgument(name) { 29 | return this._arguments?.get(name); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/context_rule.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use crate::analysis::ddsa_lib::common::{ 6 | load_function, set_key_value, set_undefined, v8_interned, DDSAJsRuntimeError, Instance, 7 | }; 8 | use deno_core::v8; 9 | use deno_core::v8::HandleScope; 10 | use std::marker::PhantomData; 11 | 12 | /// A [`v8::Global`] object created from the ES6 class `RuleContext`. 13 | #[derive(Debug)] 14 | pub struct RuleContext { 15 | v8_object: v8::Global, 16 | // Cached keys 17 | s_arguments: v8::Global, 18 | /// (See documentation on [`Instance`]). 19 | _pd: PhantomData, 20 | } 21 | 22 | impl RuleContext { 23 | /// The name of the JavaScript class. 24 | pub const CLASS_NAME: &'static str = "RuleContext"; 25 | 26 | /// Creates a new [`v8::Global`] object by loading [`Self::CLASS_NAME`] from the `scope` and creating an instance. 27 | pub fn try_new(scope: &mut HandleScope) -> Result { 28 | let js_class = load_function(scope, Self::CLASS_NAME)?; 29 | let js_class = js_class.open(scope); 30 | let args = [v8::undefined(scope).into()]; 31 | let v8_object = js_class 32 | .new_instance(scope, &args[..]) 33 | .expect("class constructor should not throw"); 34 | let v8_object = v8::Global::new(scope, v8_object); 35 | let s_arguments = v8_interned(scope, "_arguments"); 36 | let s_arguments = v8::Global::new(scope, s_arguments); 37 | Ok(Self { 38 | v8_object, 39 | s_arguments, 40 | _pd: PhantomData, 41 | }) 42 | } 43 | 44 | /// Returns a local handle to the underlying [`v8::Global`] object. 45 | pub fn as_local<'s>(&self, scope: &mut HandleScope<'s>) -> v8::Local<'s, v8::Object> { 46 | v8::Local::new(scope, &self.v8_object) 47 | } 48 | 49 | /// Sets the [`v8::Map`] containing argument names and argument values. 50 | pub fn set_arguments_map( 51 | &self, 52 | scope: &mut HandleScope, 53 | arguments_map: Option<&v8::Global>, 54 | ) { 55 | if let Some(v8_map) = arguments_map { 56 | set_key_value(&self.v8_object, scope, &self.s_arguments, |inner| { 57 | v8::Local::new(inner, v8_map).into() 58 | }); 59 | } else { 60 | set_undefined(&self.v8_object, scope, &self.s_arguments); 61 | } 62 | } 63 | 64 | /// Returns a local handle to the [`v8::Global`] map, if present. 65 | pub fn v8_arguments_map<'s>( 66 | &self, 67 | scope: &mut HandleScope<'s>, 68 | ) -> Option> { 69 | let v8_key = v8::Local::new(scope, &self.s_arguments); 70 | let v8_args = self.v8_object.open(scope).get(scope, v8_key.into()); 71 | v8_args.and_then(|value| { 72 | if value.is_undefined() { 73 | None 74 | } else { 75 | let cast: Option> = value.try_into().ok(); 76 | cast 77 | } 78 | }) 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use crate::analysis::ddsa_lib::js::context_rule::RuleContext; 85 | use crate::analysis::ddsa_lib::test_utils::{js_class_eq, js_instance_eq}; 86 | 87 | #[test] 88 | fn js_properties_canary() { 89 | let instance_expected = &[ 90 | // Variables 91 | "_arguments", 92 | // Methods 93 | "getArgument", 94 | ]; 95 | assert!(js_instance_eq(RuleContext::CLASS_NAME, instance_expected)); 96 | let class_expected = &[]; 97 | assert!(js_class_eq(RuleContext::CLASS_NAME, class_expected)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/context_ts_lang.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * @typedef {Map} MetadataMap 7 | * A map and reverse mapping for metadata about a specific facet of a tree-sitter language: 8 | * All entries will either be: 9 | * * `number` (u16) -> `string`, 10 | * * `string` -> `number` (u16) 11 | */ 12 | 13 | /** 14 | * Metadata related to a tree-sitter Language. 15 | */ 16 | export class TsLanguageContext { 17 | constructor() { 18 | /** 19 | * @type {MetadataMap} Metadata for the language's "node kind"s. 20 | * @readonly 21 | * @internal 22 | * 23 | * @remarks 24 | * This is initialized as an empty Map to give v8 a hint about the object shape. It ends 25 | * up being replaced by a v8::Global map. 26 | */ 27 | this.nodeType = new Map(); 28 | /** 29 | * @type {MetadataMap} Metadata for the language's "field"s. 30 | * @readonly 31 | * @internal 32 | * 33 | * @remarks 34 | * This is initialized as an empty Map to give v8 a hint about the object shape. It ends 35 | * up being replaced by a v8::Global map. 36 | */ 37 | this.field = new Map(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/edit.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * An object containing metadata to perform a transformation of a point range within a text. 7 | */ 8 | export class Edit { 9 | /** 10 | * @param {number} startLine 11 | * @param {number} startCol 12 | * @param {number | undefined} endLine 13 | * @param {number | undefined} endCol 14 | * @param {EditKind} kind 15 | * @param {string | undefined} content 16 | */ 17 | constructor(startLine, startCol, endLine, endCol, kind, content) { 18 | /** 19 | * The line number of the start of this edit. 20 | * @type {number} 21 | * @readonly 22 | */ 23 | this.startLine = startLine; 24 | /** 25 | * The column number of the start of this edit. 26 | * @type {number} 27 | * @readonly 28 | */ 29 | this.startCol = startCol; 30 | /** 31 | * The line number of the end of this edit. This will be `undefined` for `EditKind` "ADD". 32 | * @type {number | undefined} 33 | * @readonly 34 | */ 35 | this.endLine = endLine; 36 | /** 37 | * The column number of the end of this edit. This will be `undefined` for `EditKind` "ADD". 38 | * @type {number | undefined} 39 | * @readonly 40 | */ 41 | this.endCol = endCol; 42 | /** 43 | * @type {EditKind} 44 | * @readonly 45 | */ 46 | this.kind = kind; 47 | /** 48 | * String content associated with this edit. This will be `undefined` for `EditKind` "DELETE". 49 | * @type {string | undefined} 50 | * @readonly 51 | */ 52 | this.content = content; 53 | } 54 | 55 | /** 56 | * Creates a new edit with `EditKind` "ADD". 57 | * @param {number} startLine 58 | * @param {number} startCol 59 | * @param {string} content 60 | * @returns {Edit} 61 | */ 62 | static newAdd(startLine, startCol, content) { 63 | return new Edit(startLine, startCol, undefined, undefined, "ADD", content); 64 | } 65 | 66 | /** 67 | * Creates a new edit with `EditKind` "REMOVE". 68 | * @param {number} startLine 69 | * @param {number} startCol 70 | * @param {number} endLine 71 | * @param {number} endCol 72 | * @returns {Edit} 73 | */ 74 | static newRemove(startLine, startCol, endLine, endCol) { 75 | return new Edit(startLine, startCol, endLine, endCol, "REMOVE", undefined); 76 | } 77 | 78 | /** 79 | * Creates a new edit with `EditKind` "UPDATE". 80 | * @param {number} start_line 81 | * @param {number} start_col 82 | * @param {number} end_line 83 | * @param {number} end_col 84 | * @param {string} content The string to insert. 85 | * @returns {Edit} 86 | */ 87 | static newUpdate(start_line, start_col, end_line, end_col, content) { 88 | return new Edit(start_line, start_col, end_line, end_col, "UPDATE", content); 89 | } 90 | } 91 | 92 | /** 93 | * @typedef { "ADD" | "REMOVE" | "UPDATE"} EditKind 94 | */ 95 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/fix.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * An object containing a collection of {@link Edit}s that will fix a static analysis {@link Violation}. 7 | */ 8 | export class Fix { 9 | /** 10 | * @param {string} message 11 | * @param {Array} edits 12 | */ 13 | constructor(message, edits) { 14 | /** 15 | * A human-friendly message describing what the fix does. 16 | * @type {string} 17 | * @readonly 18 | * @private 19 | * 20 | * @remarks 21 | * This appears, for example, in the IDE in the pop-up menu, or in a Github Pull Request when suggesting a fix. 22 | */ 23 | this.message = message; 24 | /** 25 | * @type {Array} 26 | * @readonly 27 | * @private 28 | */ 29 | this.edits = edits; 30 | } 31 | 32 | /** 33 | * Creates a new `Fix``. 34 | * 35 | * @param {string} message 36 | * @param {Array} edits 37 | * @returns {Fix} 38 | * 39 | * @remarks 40 | * This is a convenience function to allow creation of a `Fix` without using the class constructor. 41 | * It is functionally equivalent to calling `new Fix(...)`. 42 | */ 43 | static new(message, edits) { 44 | return new Fix(message, edits); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/fix.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use crate::analysis::ddsa_lib::common::{ 6 | get_field, iter_v8_array, v8_type_from, DDSAJsRuntimeError, Instance, 7 | }; 8 | use crate::analysis::ddsa_lib::js::edit::EditConverter; 9 | use crate::analysis::ddsa_lib::js::Edit; 10 | use crate::analysis::ddsa_lib::v8_ds::V8Converter; 11 | use crate::model::violation; 12 | use deno_core::v8; 13 | use deno_core::v8::HandleScope; 14 | 15 | /// The JavaScript representation of a fix for a rule violation. 16 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 17 | pub struct Fix { 18 | pub message: String, 19 | pub edits: Vec>, 20 | } 21 | 22 | impl Fix { 23 | pub const CLASS_NAME: &'static str = "Fix"; 24 | } 25 | 26 | impl From> for violation::Fix { 27 | fn from(value: Fix) -> Self { 28 | let description = value.message; 29 | let edits = value 30 | .edits 31 | .into_iter() 32 | .map(violation::Edit::from) 33 | .collect::>(); 34 | violation::Fix { description, edits } 35 | } 36 | } 37 | 38 | #[derive(Debug)] 39 | pub(crate) struct FixConverter { 40 | edit_converter: EditConverter, 41 | } 42 | 43 | impl FixConverter { 44 | pub fn new() -> Self { 45 | let edit_converter = EditConverter::new(); 46 | Self { edit_converter } 47 | } 48 | } 49 | 50 | impl V8Converter for FixConverter { 51 | type Item = Fix; 52 | type Error = DDSAJsRuntimeError; 53 | 54 | fn try_convert_from<'s>( 55 | &self, 56 | scope: &mut HandleScope<'s>, 57 | value: v8::Local<'s, v8::Value>, 58 | ) -> Result { 59 | let v8_obj = v8_type_from::(value, "instanceof Fix")?; 60 | let message = get_field::(v8_obj, "message", scope, "string")? 61 | .to_rust_string_lossy(scope); 62 | let edits = get_field::(v8_obj, "edits", scope, "array")?; 63 | let edits = iter_v8_array(edits, scope) 64 | .map(|value| self.edit_converter.try_convert_from(scope, value)) 65 | .collect::, _>>()?; 66 | Ok(Fix { message, edits }) 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use crate::analysis::ddsa_lib::js::Fix; 73 | use crate::analysis::ddsa_lib::test_utils::{js_class_eq, js_instance_eq}; 74 | 75 | #[test] 76 | fn js_properties_canary() { 77 | let instance_expected = &[ 78 | // Variables 79 | "message", "edits", 80 | ]; 81 | assert!(js_instance_eq(Fix::CLASS_NAME, instance_expected)); 82 | let class_expected = &["new"]; 83 | assert!(js_class_eq(Fix::CLASS_NAME, class_expected)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/flow.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | pub(crate) mod graph; 6 | pub(crate) mod graph_test_utils; 7 | pub(crate) mod java; 8 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/globals.d.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | import {DDSA_Console} from "ext:ddsa_lib/utility"; 6 | import {QueryMatch} from "ext:ddsa_lib/query_match"; 7 | import {RootContext} from "ext:ddsa_lib/context_root"; 8 | import {TreeSitterNode} from "ext:ddsa_lib/ts_node"; 9 | import {Violation} from "ext:ddsa_lib/violation"; 10 | 11 | /** 12 | * Global variables available within a rule execution. 13 | * These are populated by `__bootstrap.js`. 14 | */ 15 | 16 | /** 17 | * @name console 18 | * @type {DDSA_Console} 19 | * @global 20 | */ 21 | 22 | /** 23 | * The context for a rule execution. 24 | * @name __RUST_BRIDGE__context 25 | * @type {RootContext} 26 | * @global 27 | */ 28 | 29 | /** 30 | * An array storing the tree-sitter query matches and their captures. The rule's `visit` function is run against each item. 31 | * @name __RUST_BRIDGE__query_match 32 | * @type {Array} 33 | * @global 34 | */ 35 | 36 | /** 37 | * A map containing all the tree-sitter nodes passed from the Rust static-analysis-kernel. 38 | * @name __RUST_BRIDGE__ts_node 39 | * @type {Map} 40 | * @global 41 | */ 42 | 43 | /** 44 | * An array storing the violations reported by the rule's JavaScript execution. 45 | * @name __RUST_BRIDGE__violation 46 | * @type {Array} 47 | * @global 48 | */ 49 | 50 | /** 51 | * The entrypoint to the ddsa runtime API. 52 | * @name ddsa 53 | * @type {DDSA} 54 | * @global 55 | */ 56 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/region.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * A contiguous portion of a file. 7 | */ 8 | export class CodeRegion { 9 | /** 10 | * @param {number} startLine 11 | * @param {number} startCol 12 | * @param {number} endLine 13 | * @param {number} endCol 14 | */ 15 | constructor(startLine, startCol, endLine, endCol) { 16 | /** 17 | * A positive integer equal to the line number containing the first character of this region. 18 | * @type {number} 19 | * @readonly 20 | */ 21 | this.startLine = startLine; 22 | /** 23 | * A positive integer equal to the column number of the first character of this region. 24 | * @type {number} 25 | * @readonly 26 | */ 27 | this.startCol = startCol; 28 | /** 29 | * A positive integer equal to the line number containing the last character of this region. 30 | * @type {number} 31 | * @readonly 32 | */ 33 | this.endLine = endLine; 34 | /** 35 | * A positive integer whose value is one greater than column number of the last character in this region. 36 | * @type {number} 37 | * @readonly 38 | */ 39 | this.endCol = endCol; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/region.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | use crate::analysis::ddsa_lib::common::{get_field, v8_type_from, DDSAJsRuntimeError, Instance}; 6 | use crate::analysis::ddsa_lib::v8_ds::V8Converter; 7 | use common::model::position; 8 | use deno_core::v8; 9 | use deno_core::v8::HandleScope; 10 | use std::marker::PhantomData; 11 | 12 | /// A representation of a JavaScript `CodeRegion` class instance. 13 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 14 | pub struct CodeRegion { 15 | pub start_line: u32, 16 | pub start_col: u32, 17 | pub end_line: u32, 18 | pub end_col: u32, 19 | /// (See documentation on [`Instance`]). 20 | pub _pd: PhantomData, 21 | } 22 | 23 | impl CodeRegion { 24 | pub const CLASS_NAME: &'static str = "CodeRegion"; 25 | } 26 | 27 | impl From> for position::Region { 28 | fn from(value: CodeRegion) -> Self { 29 | Self { 30 | start: position::Position { 31 | line: value.start_line, 32 | col: value.start_col, 33 | }, 34 | end: position::Position { 35 | line: value.end_line, 36 | col: value.end_col, 37 | }, 38 | } 39 | } 40 | } 41 | 42 | /// A struct that can convert a [`v8::Value`] to a [`CodeRegion`]. 43 | #[derive(Debug)] 44 | pub(crate) struct CodeRegionConverter; 45 | 46 | impl CodeRegionConverter { 47 | pub fn new() -> Self { 48 | Self 49 | } 50 | } 51 | 52 | impl V8Converter for CodeRegionConverter { 53 | type Item = CodeRegion; 54 | type Error = DDSAJsRuntimeError; 55 | 56 | fn try_convert_from<'s>( 57 | &self, 58 | scope: &mut HandleScope<'s>, 59 | value: v8::Local<'s, v8::Value>, 60 | ) -> Result { 61 | let v8_obj = v8_type_from::(value, "an object")?; 62 | let start_line = 63 | get_field::(v8_obj, "startLine", scope, "number")?.value() as u32; 64 | let start_col = 65 | get_field::(v8_obj, "startCol", scope, "number")?.value() as u32; 66 | let end_line = get_field::(v8_obj, "endLine", scope, "number")?.value() as u32; 67 | let end_col = get_field::(v8_obj, "endCol", scope, "number")?.value() as u32; 68 | Ok(CodeRegion { 69 | start_line, 70 | start_col, 71 | end_line, 72 | end_col, 73 | _pd: PhantomData, 74 | }) 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use crate::analysis::ddsa_lib::js::CodeRegion; 81 | use crate::analysis::ddsa_lib::test_utils::{js_class_eq, js_instance_eq}; 82 | 83 | #[test] 84 | fn js_properties_canary() { 85 | let instance_exp = &[ 86 | // Variables 87 | "startLine", 88 | "startCol", 89 | "endLine", 90 | "endCol", 91 | ]; 92 | assert!(js_instance_eq(CodeRegion::CLASS_NAME, instance_exp)); 93 | let class_expected = &[]; 94 | assert!(js_class_eq(CodeRegion::CLASS_NAME, class_expected)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/test_helpers.js: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | /** 6 | * Throws an error if `value` is not `true`. 7 | * @param {boolean} value 8 | * @param {string} message 9 | */ 10 | export function assert(value, message) { 11 | if (!value) { 12 | throw new Error(message); 13 | } 14 | } 15 | 16 | /** 17 | * Returns the {@link TreeSitterNode} with the given id, if it exists. 18 | * @param {NodeId} nid 19 | * @returns {TreeSitterNode | undefined} 20 | */ 21 | export function getNode(nid) { 22 | return globalThis.__RUST_BRIDGE__ts_node.get(nid); 23 | } 24 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/ddsa_lib/js/utility.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | #[cfg(test)] 6 | mod tests { 7 | use crate::analysis::ddsa_lib::test_utils::{cfg_test_v8, try_execute}; 8 | 9 | #[test] 10 | fn console_basic_serialization() { 11 | let mut runtime = cfg_test_v8().deno_core_rt(); 12 | let scope = &mut runtime.handle_scope(); 13 | // NOTE: There are special cases where certain class instances serialize to a different format. 14 | // These are tested in the `runtime` module, as they might require multiple bridges to be configured. 15 | let cases: &[(&str, &str)] = &[ 16 | // Single argument 17 | (r#"{ a: 123, b: "abc" }"#, r#"{"a":123,"b":"abc"}"#), 18 | ("undefined", "undefined"), 19 | ("null", "null"), 20 | ("1234", "1234"), 21 | ("12.34", "12.34"), 22 | (r#""A string""#, "A string"), 23 | ("123456789123456789n", "123456789123456789"), 24 | ("true", "true"), 25 | (r#"Symbol("abc")"#, r#"Symbol(abc)"#), 26 | ("[1, {a: 2}, 3, 4.0]", r#"[1,{"a":2},3,4]"#), 27 | // Multiple arguments 28 | (r#"1, "A string", {a: 2}"#, r#"1 A string {"a":2}"#), 29 | ]; 30 | 31 | for &(js_value, expected_serialization) in cases { 32 | let code = format!("DDSA_Console.stringifyAll({});", js_value); 33 | let value = try_execute(scope, &code).unwrap(); 34 | assert_eq!(value.to_rust_string_lossy(scope), expected_serialization); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/languages.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | pub mod csharp; 6 | pub mod go; 7 | pub mod java; 8 | pub mod javascript; 9 | pub mod python; 10 | pub mod typescript; 11 | 12 | /// Returns the text that `node` spans. 13 | /// 14 | /// This is simply a wrapper around [`tree_sitter::Node::utf8_text`] 15 | /// to make implementations less verbose while still documenting assumptions. 16 | /// 17 | /// # Panics 18 | /// This panics if the node specifies out-of-bounds indices or indices that aren't along a utf-8 19 | /// sequence boundary. This can only happen if the provided `node` is not from a tree generated from `parsed_text`. 20 | pub(crate) fn ts_node_text<'text>(parsed_text: &'text str, node: tree_sitter::Node) -> &'text str { 21 | node.utf8_text(parsed_text.as_bytes()) 22 | .expect("node should be from `parsed_text`'s tree") 23 | } 24 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/languages/csharp.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod using_directives; 6 | pub use using_directives::*; 7 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/languages/go.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod imports; 6 | pub use imports::*; 7 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/languages/java.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod imports; 6 | pub use imports::*; 7 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/languages/javascript.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod imports; 6 | pub use imports::*; 7 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/languages/python.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod imports; 6 | pub use imports::*; 7 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/analysis/languages/typescript.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod imports; 6 | pub use imports::*; 7 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/classifiers.rs: -------------------------------------------------------------------------------- 1 | // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. 2 | // This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | // Copyright 2024 Datadog, Inc. 4 | 5 | mod tests; 6 | pub use tests::is_test_file; 7 | 8 | /// Metadata associated with an artifact that has been analyzed. 9 | #[derive(Debug, Clone, Eq, PartialEq, Default)] 10 | pub struct ArtifactClassification { 11 | /// Whether the artifact is considered a "test file" or not. A test file is 12 | /// language-dependent and either contains unit tests or is associated with unit test frameworks. 13 | pub is_test_file: bool, 14 | } 15 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const VERSION: &str = "development"; 2 | pub const CARGO_VERSION: &str = env!("CARGO_PKG_VERSION"); 3 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod analysis; 2 | pub mod arguments; 3 | pub mod classifiers; 4 | pub mod config_file; 5 | pub mod constants; 6 | pub mod model; 7 | pub mod path_restrictions; 8 | pub mod rule_config; 9 | pub mod rule_overrides; 10 | pub mod utils; 11 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/model.rs: -------------------------------------------------------------------------------- 1 | pub mod analysis; 2 | pub mod common; 3 | pub mod config_file; 4 | pub mod rule; 5 | pub mod rule_test; 6 | pub mod ruleset; 7 | pub mod violation; 8 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/model/common.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Deserialize, Debug, Serialize, Eq, PartialEq)] 5 | pub enum OutputFormat { 6 | Csv, 7 | Json, 8 | Sarif, 9 | } 10 | 11 | impl fmt::Display for OutputFormat { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | let s = match self { 14 | Self::Csv => "CSV", 15 | Self::Json => "JSON", 16 | Self::Sarif => "SARIF", 17 | }; 18 | write!(f, "{s}") 19 | } 20 | } 21 | 22 | #[derive(Copy, Clone, Deserialize, Debug, Serialize, Eq, Hash, PartialEq)] 23 | pub enum Language { 24 | #[serde(rename = "CSHARP")] 25 | Csharp, 26 | #[serde(rename = "DOCKERFILE")] 27 | Dockerfile, 28 | #[serde(rename = "ELIXIR")] 29 | Elixir, 30 | #[serde(rename = "GO")] 31 | Go, 32 | #[serde(rename = "JAVA")] 33 | Java, 34 | #[serde(rename = "JAVASCRIPT")] 35 | JavaScript, 36 | #[serde(rename = "JSON")] 37 | Json, 38 | #[serde(rename = "KOTLIN")] 39 | Kotlin, 40 | #[serde(rename = "PYTHON")] 41 | Python, 42 | #[serde(rename = "RUBY")] 43 | Ruby, 44 | #[serde(rename = "RUST")] 45 | Rust, 46 | #[serde(rename = "SWIFT")] 47 | Swift, 48 | #[serde(rename = "TERRAFORM")] 49 | Terraform, 50 | #[serde(rename = "TYPESCRIPT")] 51 | TypeScript, 52 | #[serde(rename = "YAML")] 53 | Yaml, 54 | #[serde(rename = "STARLARK")] 55 | Starlark, 56 | #[serde(rename = "BASH")] 57 | Bash, 58 | PHP, 59 | #[serde(rename = "MARKDOWN")] 60 | Markdown, 61 | #[serde(rename = "APEX")] 62 | Apex, 63 | R, 64 | SQL, 65 | } 66 | 67 | #[allow(dead_code)] 68 | pub static ALL_LANGUAGES: &[Language] = &[ 69 | Language::Csharp, 70 | Language::Dockerfile, 71 | Language::Go, 72 | Language::Java, 73 | Language::JavaScript, 74 | Language::Json, 75 | Language::Kotlin, 76 | Language::Python, 77 | Language::Ruby, 78 | Language::Rust, 79 | Language::Swift, 80 | Language::TypeScript, 81 | Language::Terraform, 82 | Language::Yaml, 83 | Language::Starlark, 84 | Language::Bash, 85 | Language::PHP, 86 | Language::Markdown, 87 | Language::Apex, 88 | Language::R, 89 | Language::SQL, 90 | ]; 91 | 92 | impl fmt::Display for Language { 93 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 94 | let s = match self { 95 | Self::Csharp => "c#", 96 | Self::Dockerfile => "dockerfile", 97 | Self::Go => "go", 98 | Self::Elixir => "elixir", 99 | Self::Java => "java", 100 | Self::JavaScript => "javascript", 101 | Self::Json => "json", 102 | Self::Kotlin => "kotlin", 103 | Self::Python => "python", 104 | Self::Ruby => "ruby", 105 | Self::Rust => "rust", 106 | Self::Swift => "swift", 107 | Self::Terraform => "terraform", 108 | Self::TypeScript => "typescript", 109 | Self::Yaml => "yaml", 110 | Self::Starlark => "starlark", 111 | Self::Bash => "bash", 112 | Self::PHP => "php", 113 | Self::Markdown => "markdown", 114 | Self::Apex => "apex", 115 | Self::R => "r", 116 | Self::SQL => "sql", 117 | }; 118 | write!(f, "{s}") 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/model/rule_test.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct RuleTest { 5 | pub annotation_count: u32, 6 | pub filename: String, 7 | #[serde(rename = "code")] 8 | pub code_base64: String, 9 | } 10 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/model/violation.rs: -------------------------------------------------------------------------------- 1 | use crate::model::rule::{RuleCategory, RuleSeverity}; 2 | use common::model::position::{Position, Region}; 3 | 4 | use derive_builder::Builder; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Copy, Clone, Deserialize, Debug, Serialize, Eq, PartialEq)] 8 | pub enum EditType { 9 | #[serde(rename = "ADD")] 10 | Add, 11 | #[serde(rename = "REMOVE")] 12 | Remove, 13 | #[serde(rename = "UPDATE")] 14 | Update, 15 | } 16 | 17 | #[derive(Deserialize, Debug, Serialize, Clone, Builder)] 18 | pub struct Edit { 19 | pub start: Position, 20 | pub end: Option, 21 | pub edit_type: EditType, 22 | pub content: Option, 23 | } 24 | 25 | #[derive(Deserialize, Debug, Serialize, Clone, Builder)] 26 | pub struct Fix { 27 | pub description: String, 28 | pub edits: Vec, 29 | } 30 | 31 | #[derive(Deserialize, Debug, Serialize, Clone, Builder)] 32 | pub struct Violation { 33 | pub start: Position, 34 | pub end: Position, 35 | pub message: String, 36 | pub severity: RuleSeverity, 37 | pub category: RuleCategory, 38 | pub fixes: Vec, 39 | /// An ordered list of regions representing a flow from start to finish. 40 | pub taint_flow: Option>, 41 | } 42 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/rule_config.rs: -------------------------------------------------------------------------------- 1 | use crate::arguments::ArgumentProvider; 2 | use crate::model::config_file::{split_path, ConfigFile, SplitPath}; 3 | use crate::model::rule::{RuleCategory, RuleSeverity}; 4 | use crate::path_restrictions::PathRestrictions; 5 | use crate::rule_overrides::RuleOverrides; 6 | use common::model::diff_aware::DiffAware; 7 | use std::collections::HashMap; 8 | use std::sync::OnceLock; 9 | 10 | #[derive(Default, Clone)] 11 | pub struct RuleConfigProvider { 12 | path_restrictions: PathRestrictions, 13 | argument_provider: ArgumentProvider, 14 | rule_overrides: RuleOverrides, 15 | } 16 | 17 | impl RuleConfigProvider { 18 | pub fn from_config(cfg: &ConfigFile) -> RuleConfigProvider { 19 | RuleConfigProvider { 20 | path_restrictions: PathRestrictions::from_ruleset_configs(&cfg.rulesets), 21 | argument_provider: ArgumentProvider::from(cfg), 22 | rule_overrides: RuleOverrides::from_config_file(cfg), 23 | } 24 | } 25 | 26 | pub fn config_for_file(&self, file_path: &str) -> RuleConfig { 27 | RuleConfig { 28 | provider: self, 29 | file_path: file_path.to_string(), 30 | split_path: split_path(file_path), 31 | } 32 | } 33 | } 34 | 35 | impl DiffAware for RuleConfigProvider { 36 | fn generate_diff_aware_digest(&self) -> String { 37 | format!( 38 | "{}:{}", 39 | self.path_restrictions.generate_diff_aware_digest(), 40 | self.argument_provider.generate_diff_aware_digest() 41 | ) 42 | } 43 | } 44 | 45 | pub struct RuleConfig<'a> { 46 | provider: &'a RuleConfigProvider, 47 | file_path: String, 48 | split_path: SplitPath, 49 | } 50 | 51 | impl RuleConfig<'_> { 52 | pub fn rule_is_enabled(&self, rule_name: &str) -> bool { 53 | self.provider 54 | .path_restrictions 55 | .rule_applies(rule_name, &self.file_path) 56 | } 57 | 58 | pub fn get_arguments(&self, rule_name: &str) -> HashMap { 59 | self.provider 60 | .argument_provider 61 | .get_arguments(&self.split_path, rule_name) 62 | } 63 | 64 | pub fn get_severity(&self, rule_name: &str) -> Option { 65 | self.provider 66 | .rule_overrides 67 | .severity(&self.split_path, rule_name) 68 | } 69 | 70 | pub fn get_category(&self, rule_name: &str) -> Option { 71 | self.provider.rule_overrides.category(rule_name) 72 | } 73 | } 74 | 75 | impl Default for RuleConfig<'static> { 76 | fn default() -> Self { 77 | static PROVIDER: OnceLock = OnceLock::new(); 78 | PROVIDER 79 | .get_or_init(RuleConfigProvider::default) 80 | .config_for_file("") 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/rule_overrides.rs: -------------------------------------------------------------------------------- 1 | use crate::model::config_file::{BySubtree, ConfigFile, SplitPath}; 2 | use crate::model::rule::{RuleCategory, RuleSeverity}; 3 | use std::collections::HashMap; 4 | 5 | /// User-provided overrides for rule definitions. 6 | #[derive(Default, Clone)] 7 | pub struct RuleOverrides { 8 | severities: HashMap>, 9 | categories: HashMap, 10 | } 11 | 12 | impl RuleOverrides { 13 | // Reads the overrides from the configuration file. 14 | pub fn from_config_file(cfg: &ConfigFile) -> Self { 15 | let severities: HashMap> = cfg 16 | .rulesets 17 | .iter() 18 | .flat_map(|(rs_name, cfg)| { 19 | cfg.rules.iter().filter_map(move |(rule_name, rule)| { 20 | rule.severity 21 | .as_ref() 22 | .map(|sev| (format!("{}/{}", rs_name, rule_name), sev.clone())) 23 | }) 24 | }) 25 | .collect(); 26 | let categories: HashMap = cfg 27 | .rulesets 28 | .iter() 29 | .flat_map(|(rs_name, cfg)| { 30 | cfg.rules.iter().filter_map(move |(rule_name, rule)| { 31 | rule.category 32 | .as_ref() 33 | .map(|cat| (format!("{}/{}", rs_name, rule_name), *cat)) 34 | }) 35 | }) 36 | .collect(); 37 | RuleOverrides { 38 | severities, 39 | categories, 40 | } 41 | } 42 | 43 | // Returns the overridden severity for the given rule name, or the original severity if no override exists. 44 | pub fn severity(&self, file_path: &SplitPath, rule_name: &str) -> Option { 45 | self.severities 46 | .get(rule_name) 47 | .and_then(|s| s.get_ancestor(file_path).cloned()) 48 | } 49 | 50 | // Returns the overridden category for the given rule name, or the original category if no override exists. 51 | pub fn category(&self, rule_name: &str) -> Option { 52 | self.categories.get(rule_name).copied() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/static-analysis-kernel/src/utils.rs: -------------------------------------------------------------------------------- 1 | use base64::engine::general_purpose; 2 | use base64::Engine; 3 | 4 | pub fn decode_base64_string(base64_string: String) -> anyhow::Result { 5 | anyhow::Ok(String::from_utf8( 6 | general_purpose::STANDARD.decode(base64_string)?, 7 | )?) 8 | } 9 | 10 | pub fn encode_base64_string(str: String) -> String { 11 | general_purpose::STANDARD.encode(str) 12 | } 13 | -------------------------------------------------------------------------------- /crates/static-analysis-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static-analysis-server" 3 | edition = "2021" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | # local 8 | common = { package = "common", path = "../common" } 9 | kernel = { package = "static-analysis-kernel", path = "../static-analysis-kernel" } 10 | # workspace 11 | anyhow = { workspace = true } 12 | base64 = { workspace = true } 13 | derive_builder = { workspace = true } 14 | serde = { workspace = true } 15 | serde_json = { workspace = true } 16 | tracing = { workspace = true } 17 | 18 | [dev-dependencies] 19 | assert-json-diff = "2.0.2" 20 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/constants.rs: -------------------------------------------------------------------------------- 1 | // when a rule failed to parse 2 | pub const ERROR_PARSING_RULE: &str = "error-parsing-rule"; 3 | // when a rule is not base64 4 | pub const ERROR_DECODING_BASE64: &str = "error-decoding-base64"; 5 | // when the code is not base64 6 | pub const ERROR_CODE_NOT_BASE64: &str = "code-not-base64"; 7 | // when the configuration file is not valid base64 8 | pub const ERROR_CONFIGURATION_NOT_BASE64: &str = "configuration-not-base64"; 9 | // when it was not possible to parse the configuration file 10 | pub const ERROR_COULD_NOT_PARSE_CONFIGURATION: &str = "could-not-parse-configuration"; 11 | // rules and core language are different 12 | pub const ERROR_CODE_LANGUAGE_MISMATCH: &str = "language-mismatch"; 13 | // no root node when trying to get the AST 14 | pub const ERROR_CODE_NO_ROOT_NODE: &str = "no-root-node"; 15 | pub const ERROR_CHECKSUM_MISMATCH: &str = "checksum-mismatch"; 16 | 17 | pub const SERVER_HEADER_SHUTDOWN_ENABLED: &str = "X-static-analyzer-server-shutdown-enabled"; 18 | pub const SERVER_HEADER_KEEPALIVE_ENABLED: &str = "X-static-analyzer-server-keepalive-enabled"; 19 | pub const SERVER_HEADER_RULE_CACHE_ENABLED: &str = "X-static-analyzer-server-rule-cache-enabled"; 20 | pub const SERVER_HEADER_RULE_TIMEOUT: &str = "X-static-analyzer-server-rule-timeout"; 21 | pub const SERVER_HEADER_SERVER_VERSION: &str = "X-static-analyzer-server-version"; 22 | pub const SERVER_HEADER_SERVER_REVISION: &str = "X-static-analyzer-server-revision"; 23 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod model; 3 | pub mod request; 4 | pub mod tree_sitter_tree; 5 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/model.rs: -------------------------------------------------------------------------------- 1 | pub mod analysis_request; 2 | pub mod analysis_response; 3 | pub mod tree_sitter_tree_node; 4 | pub mod tree_sitter_tree_request; 5 | pub mod tree_sitter_tree_response; 6 | pub mod violation; 7 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/model/analysis_response.rs: -------------------------------------------------------------------------------- 1 | use crate::model::violation::ServerViolation; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Deserialize, Debug, Serialize)] 5 | pub struct RuleResponse { 6 | pub identifier: String, 7 | pub violations: Vec, 8 | pub errors: Vec, 9 | pub execution_error: Option, 10 | pub output: Option, 11 | pub execution_time_ms: u128, 12 | } 13 | 14 | #[derive(Clone, Deserialize, Debug, Serialize)] 15 | pub struct AnalysisResponse { 16 | pub rule_responses: Vec, 17 | pub errors: Vec, 18 | } 19 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/model/tree_sitter_tree_node.rs: -------------------------------------------------------------------------------- 1 | use common::model::position::Position; 2 | use kernel::model::analysis::TreeSitterNode; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | // This representation is for the server only for an node representation. In the kernel, 6 | // we serialize/deserialize in camelCase since the value is retrieved in JavaScript code. 7 | // The API only emits camel_case_code, which is why we have this class. 8 | #[derive(Clone, Deserialize, Debug, Serialize)] 9 | pub struct ServerTreeSitterNode { 10 | pub ast_type: String, 11 | pub start: Position, 12 | pub end: Position, 13 | pub field_name: Option, 14 | pub children: Vec, 15 | } 16 | 17 | impl From for ServerTreeSitterNode { 18 | fn from(value: TreeSitterNode) -> Self { 19 | ServerTreeSitterNode { 20 | ast_type: value.ast_type, 21 | start: value.start, 22 | end: value.end, 23 | field_name: value.field_name, 24 | children: value 25 | .children 26 | .into_iter() 27 | .map(ServerTreeSitterNode::from) 28 | .collect(), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/model/tree_sitter_tree_request.rs: -------------------------------------------------------------------------------- 1 | use kernel::model::common::Language; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Deserialize, Debug, Serialize)] 5 | pub struct TreeSitterRequest { 6 | pub language: Language, 7 | pub file_encoding: String, 8 | #[serde(rename = "code")] 9 | pub code_base64: String, 10 | } 11 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/model/tree_sitter_tree_response.rs: -------------------------------------------------------------------------------- 1 | use crate::model::tree_sitter_tree_node::ServerTreeSitterNode; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Deserialize, Debug, Serialize)] 5 | pub struct TreeSitterResponse { 6 | pub result: Option, 7 | pub errors: Vec, 8 | } 9 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/model/violation.rs: -------------------------------------------------------------------------------- 1 | use kernel::model::violation::Violation; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// A newtype [`Violation`] that provides a custom serialization for the server. 5 | #[derive(Deserialize, Debug, Serialize, Clone)] 6 | #[serde(transparent)] 7 | pub struct ServerViolation(pub Violation); 8 | 9 | impl From for ServerViolation { 10 | fn from(value: Violation) -> Self { 11 | Self(value) 12 | } 13 | } 14 | 15 | impl<'a> From<&'a Violation> for ServerViolation { 16 | fn from(value: &'a Violation) -> Self { 17 | Self(value.clone()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/static-analysis-server/src/tree_sitter_tree.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{ERROR_CODE_NOT_BASE64, ERROR_CODE_NO_ROOT_NODE}; 2 | use crate::model::tree_sitter_tree_node::ServerTreeSitterNode; 3 | use crate::model::tree_sitter_tree_request::TreeSitterRequest; 4 | use crate::model::tree_sitter_tree_response::TreeSitterResponse; 5 | use kernel::analysis::tree_sitter::{get_tree, map_node}; 6 | use kernel::utils::decode_base64_string; 7 | 8 | // Return the tree for the language and code sent as parameter. 9 | #[tracing::instrument(skip_all)] 10 | pub fn process_tree_sitter_tree_request(request: TreeSitterRequest) -> TreeSitterResponse { 11 | tracing::debug!("Processing tree-sitter AST generation request"); 12 | 13 | let no_root_node = TreeSitterResponse { 14 | result: None, 15 | errors: vec![ERROR_CODE_NO_ROOT_NODE.to_string()], 16 | }; 17 | 18 | let Ok(decoded) = decode_base64_string(request.code_base64) else { 19 | tracing::info!("Validation error: code is not a base64 string"); 20 | return TreeSitterResponse { 21 | result: None, 22 | errors: vec![ERROR_CODE_NOT_BASE64.to_string()], 23 | }; 24 | }; 25 | 26 | tracing::debug!( 27 | "Getting tree-sitter tree (code length: {} bytes)", 28 | &decoded.len() 29 | ); 30 | 31 | // Note: [get_tree] returns None if the call to tree_sitter::Parser::set_language returns an Err 32 | let result = get_tree(&decoded, &request.language); 33 | if result.is_none() { 34 | tracing::warn!( 35 | "Unable to create tree-sitter parser for language `{}`", 36 | request.language 37 | ); 38 | return no_root_node; 39 | } 40 | 41 | if let Some(result) = 42 | result.map(|tree| map_node(tree.root_node()).map(ServerTreeSitterNode::from)) 43 | { 44 | tracing::info!("Successfully completed tree-sitter AST generation"); 45 | TreeSitterResponse { 46 | result, 47 | errors: vec![], 48 | } 49 | } else { 50 | tracing::info!("Generated AST contained no root node"); 51 | no_root_node 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use kernel::model::common::Language; 58 | 59 | use super::*; 60 | 61 | #[test] 62 | fn test_process_tree_sitter_tree_request_happy_path() { 63 | let request = TreeSitterRequest { 64 | code_base64: "ZnVuY3Rpb24gdmlzaXQobm9kZSwgZmlsZW5hbWUsIGNvZGUpIHsKICAgIGNvbnN0IGZ1bmN0aW9uTmFtZSA9IG5vZGUuY2FwdHVyZXNbIm5hbWUiXTsKICAgIGlmKGZ1bmN0aW9uTmFtZSkgewogICAgICAgIGNvbnN0IGVycm9yID0gYnVpbGRFcnJvcihmdW5jdGlvbk5hbWUuc3RhcnQubGluZSwgZnVuY3Rpb25OYW1lLnN0YXJ0LmNvbCwgZnVuY3Rpb25OYW1lLmVuZC5saW5lLCBmdW5jdGlvbk5hbWUuZW5kLmNvbCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImludmFsaWQgbmFtZSIsICJDUklUSUNBTCIsICJzZWN1cml0eSIpOwoKICAgICAgICBjb25zdCBlZGl0ID0gYnVpbGRFZGl0KGZ1bmN0aW9uTmFtZS5zdGFydC5saW5lLCBmdW5jdGlvbk5hbWUuc3RhcnQuY29sLCBmdW5jdGlvbk5hbWUuZW5kLmxpbmUsIGZ1bmN0aW9uTmFtZS5lbmQuY29sLCAidXBkYXRlIiwgImJhciIpOwogICAgICAgIGNvbnN0IGZpeCA9IGJ1aWxkRml4KCJ1c2UgYmFyIiwgW2VkaXRdKTsKICAgICAgICBhZGRFcnJvcihlcnJvci5hZGRGaXgoZml4KSk7CiAgICB9Cn0=".to_string(), 65 | file_encoding: "utf-8".to_string(), 66 | language: Language::Python, 67 | }; 68 | let response = process_tree_sitter_tree_request(request); 69 | assert!(response.errors.is_empty()); 70 | assert!(response.result.is_some()); 71 | assert_eq!("module", response.result.unwrap().ast_type); 72 | } 73 | 74 | #[test] 75 | fn test_process_tree_sitter_invalid_base64() { 76 | let request = TreeSitterRequest { 77 | code_base64: "we2323423423090909)()(&(*&!@!@=".to_string(), 78 | file_encoding: "utf-8".to_string(), 79 | language: Language::Python, 80 | }; 81 | let response = process_tree_sitter_tree_request(request); 82 | assert_eq!( 83 | &ERROR_CODE_NOT_BASE64.to_string(), 84 | response.errors.get(0).unwrap() 85 | ); 86 | assert!(response.result.is_none()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /doc/docker-container.md: -------------------------------------------------------------------------------- 1 | # Docker Container 2 | 3 | `datadog-static-analyzer` is available for use in a Docker container. The 4 | container is published to the [GHCR registry](https://github.com/DataDog/datadog-static-analyzer/pkgs/container/datadog-static-analyzer). 5 | 6 | ## Requirements 7 | 8 | - [Docker](https://docs.docker.com/get-docker/) 9 | - An x86_64 or arm64 machine 10 | 11 | ## Setup 12 | 13 | First, pull the container image: 14 | 15 | ```sh 16 | docker pull ghcr.io/datadog/datadog-static-analyzer:latest 17 | ``` 18 | 19 | Then, run the container to verify that it works: 20 | 21 | ```sh 22 | docker run ghcr.io/datadog/datadog-static-analyzer:latest --help 23 | ``` 24 | 25 | ## Usage 26 | 27 | The container can be run in the same way as the binary. For example, to 28 | run the analyzer on a directory stored at `$PATH_TO_ANALYZE`: 29 | 30 | ```sh 31 | docker run -v $PATH_TO_ANALYZE:/data ghcr.io/datadog/datadog-static-analyzer:latest -i /data -o /data/output.sarif -b -f sarif 32 | ``` 33 | 34 | ### Using datadog-ci 35 | 36 | The `datadog-ci` tool is also available in the container, and can be used to 37 | upload your results to the Datadog app. To use it, run the container with the 38 | `datadog-ci` command by overriding the entrypoint: 39 | 40 | ```sh 41 | docker run --entrypoint datadog-ci ghcr.io/datadog/datadog-static-analyzer:latest --help 42 | ``` 43 | 44 | ## Building the container from source 45 | 46 | To build the container from source, clone the repository and run the following: 47 | 48 | ```sh 49 | docker build -t datadog-static-analyzer . 50 | ``` 51 | 52 | Then, run the locally-built container: 53 | 54 | ```sh 55 | docker run datadog-static-analyzer --help 56 | ``` 57 | 58 | ## Pinning the container 59 | 60 | If you are interested in pinning the container to a specific version, each release 61 | is tagged with the version name as well. For example, to use version `0.3.5`: 62 | 63 | ```sh 64 | docker pull ghcr.io/datadog/datadog-static-analyzer:0.3.5 65 | ``` 66 | 67 | ## Issues 68 | 69 | If you encounter any issues, please open an issue [here](https://github.com/DataDog/datadog-static-analyzer/issues/new?assignees=&labels=&projects=&template=bug_report.md&title=). 70 | -------------------------------------------------------------------------------- /doc/git-hooks.md: -------------------------------------------------------------------------------- 1 | ## Git Hooks 2 | 3 | Git hooks are supported for Datadog Static Analyzer. Using 4 | Git hooks prevent you from pushing or committing any code that contains 5 | a violation or a secret. 6 | 7 | ### How to use 8 | 9 | The `datadog-static-analyzer-git-hook` program ([source](../crates/bins/src/bin/datadog-static-analyzer-git-hook)) 10 | is designed to be integrated in [Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), 11 | either `pre-commit` or `pre-push`. 12 | 13 | The program needs to be in the `$PATH` environment variable of the current user. 14 | 15 | You have two modes to invoke the program: 16 | 17 | - compare the current branch with a default branch 18 | - compare two shas 19 | 20 | ### Compare the current branch with a default branch 21 | 22 | To compare the current commit with the default branch main, invoke the following command. 23 | 24 | ```shell 25 | datadog-static-analyzer-git-hook -r --enable-secrets true --default-branch main 26 | ``` 27 | 28 | ### Compare two commit shas 29 | 30 | To compare the commit `` and `` (`` has been committed before ``), enter the following command. 31 | 32 | ```shell 33 | datadog-static-analyzer-git-hook -r --enable-static-analysis true --enable-secrets true --sha-start --sha-end 34 | ``` 35 | 36 | ### Options 37 | 38 | - `--enable-static-analysis`: check for any static analysis error (value: true/false) 39 | - `--enable-secrets`: also validate secrets (value: true/false) 40 | with [Datadog Sensitive Data Scanner](https://docs.datadoghq.com/sensitive_data_scanner/) 41 | - `--confirmation`: prompts the user if they want to bypass the warning 42 | 43 | ### Example of Integration 44 | 45 | #### Step 1: ensure that `datadog-static-analyzer-git-hook` is in your PATH 46 | 47 | Make sure you copy the `datadog-static-analyzer-git-hook` in your `PATH` and you can invoke it. 48 | 49 | #### Step 2: add Git `pre-commit` script 50 | 51 | In your repository, create a file `.git/hooks/pre-push` with the following content. 52 | With this file, the datadog static analyzer will check that code being pushed does 53 | not contain any violation or secret. It will also ask the user if they want to bypass 54 | the verification (`--confirmation`). 55 | 56 | ```shell 57 | 58 | #!/bin/sh 59 | 60 | # Get the repo root path 61 | repo_path=$(git rev-parse --show-toplevel) 62 | 63 | # Make sure the user can provide some input 64 | exec < /dev/tty 65 | 66 | datadog-static-analyzer-git-hook -r $repo_path --enable-secrets true --enable-static-analysis true --confirmation --default-branch main 67 | 68 | if [ $? -eq 0 ]; then 69 | echo "datadog-static-analyzer check passed" 70 | exit 0 71 | else 72 | echo "datadog-static-analyzer check failed" 73 | exit 1 74 | fi 75 | ``` 76 | 77 | Make sure the file is executable 78 | 79 | ```shell 80 | chmod a+x .git/hooks/pre-push 81 | ``` 82 | 83 | Once configure, the script will be called each time you push. It will provide 84 | an output like the following: 85 | 86 | ![Datadog Static Analysis Git Hook](imgs/git-hook.jpeg) 87 | 88 | 89 | -------------------------------------------------------------------------------- /doc/go.md: -------------------------------------------------------------------------------- 1 | ## Go Support 2 | 3 | 4 | ### Helpers 5 | 6 | #### Packages and Aliases 7 | 8 | Consider the following code: 9 | 10 | ```go 11 | import ( 12 | "fmt" 13 | 14 | fmt2 "fmt" 15 | ) 16 | 17 | 18 | ``` 19 | 20 | The `query.context.packages` contains the list of packages being imported. In the code above, it will contain a single element: `"fmt"` 21 | 22 | The `query.context.packages_aliased` contains a map of the packages being imported with their aliases. If a package is not aliased, it will just contain it's value. For the code above, the map contains the following key/value: 23 | - "fmt" -> "fmt" 24 | - "fmt2" -> "fmt" 25 | 26 | 27 | Let's now consider another code 28 | 29 | ```go 30 | import ( 31 | "errors" 32 | "fmt" 33 | "net/http" 34 | ) 35 | ``` 36 | 37 | - `query.context.packages` will contain the values `errors`, `fmt` and `net/http` 38 | - `query.context.packages_aliased` will contain the following values 39 | - `errors` -> `errors` 40 | - `fmt` -> `fmt` 41 | - `http` -> `net/http` 42 | 43 | -------------------------------------------------------------------------------- /doc/ignore-rule.md: -------------------------------------------------------------------------------- 1 | ## Ignoring a rule 2 | 3 | 4 | > [!NOTE] 5 | > TL;DR: use the command `no-dd-sa` to ignore a rule 6 | 7 | 8 | ### Ignoring all results in a file 9 | 10 | To ignore all rules in a file, put `no-dd-sa` in a comment 11 | on the **first line** of the file. 12 | 13 | In the following code snippet, all rules are ignored in the file. 14 | 15 | ```python 16 | #no-dd-sa 17 | def foo(): 18 | print("foo: {}".format("bar")) 19 | ``` 20 | 21 | 22 | 23 | ### Ignoring a list of rules in a file 24 | 25 | To ignore all rules in a file, put `no-dd-sa` followed 26 | by the list of rules to ignore in a comment 27 | on the **first line** of the file. 28 | 29 | In the following code snippets, the rules `python-best-practices/rule1` 30 | and `python-best-practices/rule2` will be ignored for the file, all 31 | other rules will apply. 32 | 33 | ```python 34 | #no-dd-sa python-best-practices/rule1 python-best-practices/rule2 35 | def foo(): 36 | print("foo: {}".format("bar")) 37 | ``` 38 | 39 | 40 | ### Ignoring a list of rules on a specific line 41 | 42 | To ignore rules on a specific line, put `no-dd-sa` followed 43 | by the list of rules to ignore in a comment on the **line above** 44 | the one you want to ignore. 45 | 46 | ```python 47 | def foo(): 48 | #no-dd-sa python-best-practices/rule1 python-best-practices/rule2 49 | print("foo: {}".format("bar")) 50 | ``` 51 | 52 | 53 | ### Ignoring all rules on a specific line 54 | 55 | To ignore all rules on a specific line, put `no-dd-sa` on the **line above** 56 | the one you want to ignore. 57 | 58 | ```python 59 | def foo(): 60 | #no-dd-sa 61 | print("foo: {}".format("bar")) 62 | ``` 63 | -------------------------------------------------------------------------------- /doc/imgs/git-hook.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/datadog-static-analyzer/09dd4060cdc193b7075762e7f0947e283e512188/doc/imgs/git-hook.jpeg -------------------------------------------------------------------------------- /doc/imgs/owasp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/datadog-static-analyzer/09dd4060cdc193b7075762e7f0947e283e512188/doc/imgs/owasp.png -------------------------------------------------------------------------------- /doc/owasp-benchmark.md: -------------------------------------------------------------------------------- 1 | ## Running OWASP benchmark 2 | 3 | ### Requirements 4 | 5 | - Java 11 ([OpenJDK for example](https://openjdk.org/install/)) 6 | - [Maven](https://maven.apache.org/install.html) 7 | - [Rust toolchain](https://rustup.rs/) 8 | 9 | 10 | ### Benchmark results 11 | 12 | These are the results of the OWASP Benchmark for the Datadog Static Analyzer as of 05/15/2025. 13 | 14 | ![Datadog Static Analysis OWASP Benchmark](imgs/owasp.png) 15 | 16 | 17 | ### Step 1: Clone and install the BenchmarkUtils repository 18 | The [BenchmarkUtils](https://github.com/juli1/BenchmarkUtils) repository has utility methods to parse SARIF files from our static analyzer and associate CWE. We updated it to support our SARIF format. 19 | 20 | First, clone the repository 21 | 22 | ``` 23 | git clone https://github.com/juli1/BenchmarkUtils 24 | ``` 25 | 26 | Switch to the branch `juli1/add-datadog-support` 27 | 28 | ``` 29 | cd BenchmarkUtils && git checkout juli1/add-datadog-support 30 | ``` 31 | 32 | Then, compile and install the library. It will install it in your local maven repository. 33 | 34 | ``` 35 | mvn compile && mvn install 36 | ``` 37 | 38 | 39 | ### Step 2: Clone BenchmarkJava 40 | The [BenchmarkJava](https://github.com/OWASP-Benchmark/BenchmarkJava.git) repository contains 41 | all the code to analyze and generate the scorecards. 42 | 43 | Clone it on your local machine. 44 | 45 | ``` 46 | git clone https://github.com/OWASP-Benchmark/BenchmarkJava.git 47 | ``` 48 | 49 | 50 | ### Step 3: Run our static analyzer 51 | 52 | Clone our static analyzer using the following command. 53 | 54 | ``` 55 | git clone https://github.com/DataDog/datadog-static-analyzer.git 56 | 57 | ``` 58 | 59 | Then, run it. At the top directory of the static analyzer, use 60 | 61 | ``` 62 | cd datadog-static-analyzer && cargo run --locked --release --bin datadog-static-analyzer -- --format sarif --output /path/to/BenchmarkJava/results/Benchmark_1.2-DatadogSast.sarif --directory /path/to/BenchmarkJava 63 | ``` 64 | 65 | 66 | ### Step 4: Create Scorecards 67 | 68 | Create the scorecards from the `BenchmarkJava` repository (the one created during step 2). 69 | 70 | ``` 71 | cd /path/to/BenchmarkJava/ && ./createScorecards.sh 72 | 73 | ``` 74 | 75 | The scorecards are then in the `scorecard/` directory. 76 | -------------------------------------------------------------------------------- /doc/report-issue.md: -------------------------------------------------------------------------------- 1 | ## Report an issue 2 | 3 | This document outlines how to report an issue for the static analyzer and 4 | ensure that your bug report is complete with all required information. 5 | 6 | > [!TIP] 7 | > If you are a Datadog customer, send the bug report with all information 8 | > to your customer success manager. 9 | 10 | ### What to send with your report 11 | 12 | When sending a bug report, make sure to include 13 | - your `static-analysis.datadog.yml` file 14 | - the output of the tool (e.g. CLI) when running either locally or in your CI/CD pipeline 15 | - the SARIF file being produced (if any and available) 16 | - the URL of your repository (even if private, it helps to troubleshoot) 17 | - the exact command line used to run the analyzer 18 | 19 | ### Performance issues 20 | 21 | If you are experiencing performance issues, enable the flag `--performance-statistics` when 22 | running the tool from the command line. 23 | 24 | When sending the report, make sure to include: 25 | - your `static-analysis.datadog.yml` 26 | - the output of the tool 27 | - the URL of your repository 28 | 29 | > [!IMPORTANT] 30 | > If you are using the [GitHub Action](https://github.com/DataDog/datadog-static-analyzer-github-action) 31 | > just turn on the option `enable_performance_statistics` to `true`. 32 | 33 | ### Blocking issues 34 | 35 | If the static analyzer fails to exit or you have a major issue that is not related to performance, 36 | run the analyzer with the following flags: `--debug true --performance-statistics`. 37 | 38 | -------------------------------------------------------------------------------- /misc/github-action.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -l 2 | 3 | ######################################################## 4 | # Check variables 5 | ######################################################## 6 | if [ -z "$DD_API_KEY" ]; then 7 | echo "DD_API_KEY not set. Please set one and try again." 8 | exit 1 9 | fi 10 | 11 | if [ -z "$DD_APP_KEY" ]; then 12 | echo "DD_APP_KEY not set. Please set one and try again." 13 | exit 1 14 | fi 15 | 16 | if [ -n "$DD_ENV" ]; then 17 | echo "::warning title=Deprecated environment variable::DD_ENV has been set, but it is no longer functional. This warning will be removed in a future update." 18 | fi 19 | 20 | if [ -n "$DD_SERVICE" ]; then 21 | echo "::warning title=Deprecated environment variable::DD_SERVICE has been set, but it is no longer functional. This warning will be removed in a future update." 22 | fi 23 | 24 | if [ -z "$CPU_COUNT" ]; then 25 | # the default CPU count is 2 26 | CPU_COUNT=2 27 | fi 28 | 29 | if [ "$ENABLE_PERFORMANCE_STATISTICS" = "true" ]; then 30 | ENABLE_PERFORMANCE_STATISTICS="--performance-statistics" 31 | else 32 | ENABLE_PERFORMANCE_STATISTICS="" 33 | fi 34 | 35 | if [ "$ENABLE_DEBUG" = "yes" ]; then 36 | DEBUG_ARGUMENT_VALUE="yes" 37 | else 38 | DEBUG_ARGUMENT_VALUE="no" 39 | fi 40 | 41 | if [ -n "$SUBDIRECTORY" ]; then 42 | for subdirectory in $SUBDIRECTORY; do 43 | SUBDIRECTORY_OPTION="$SUBDIRECTORY_OPTION --subdirectory $subdirectory" 44 | done 45 | fi 46 | 47 | if [ "$DIFF_AWARE" = "true" ]; then 48 | DIFF_AWARE_VALUE="--diff-aware" 49 | else 50 | DIFF_AWARE_VALUE="" 51 | fi 52 | 53 | if [ "$SECRETS_ENABLED" = "true" ]; then 54 | SECRETS_ENABLED_VALUE="--enable-secrets true" 55 | else 56 | SECRETS_ENABLED_VALUE="" 57 | fi 58 | 59 | if [ "$STATIC_ANALYSIS_ENABLED" = "false" ]; then 60 | STATIC_ANALYSIS_ENABLED_VALUE="--enable-static-analysis false" 61 | else 62 | STATIC_ANALYSIS_ENABLED_VALUE="--enable-static-analysis true" 63 | fi 64 | 65 | ######################################################## 66 | # Output directory 67 | ######################################################## 68 | echo "Getting output directory" 69 | OUTPUT_DIRECTORY=$(mktemp -d) 70 | 71 | # Check that datadog-ci was installed 72 | if [ ! -d "$OUTPUT_DIRECTORY" ]; then 73 | echo "Output directory ${OUTPUT_DIRECTORY} does not exist" 74 | exit 1 75 | fi 76 | 77 | OUTPUT_FILE="$OUTPUT_DIRECTORY/output.sarif" 78 | 79 | echo "Done: will output results at $OUTPUT_FILE" 80 | 81 | ######################################################## 82 | # Execute the tool and upload results 83 | ######################################################## 84 | 85 | # Navigate to workspace root, so the datadog-ci command can access the git info 86 | cd $GITHUB_WORKSPACE || exit 1 87 | git config --global --add safe.directory $GITHUB_WORKSPACE || exit 1 88 | 89 | # Only upload git metadata if diff aware is enabled. 90 | if [ "$DIFF_AWARE" = "true" ]; then 91 | echo "Disabling extensions.worktreeConfig" 92 | git config --unset extensions.worktreeConfig 93 | echo "Done" 94 | 95 | echo "Upload git metadata" 96 | datadog-ci git-metadata upload 97 | echo "Done" 98 | fi 99 | 100 | echo "Starting Static Analysis" 101 | datadog-static-analyzer -i "$GITHUB_WORKSPACE" -g -o "$OUTPUT_FILE" -f sarif --cpus "$CPU_COUNT" "$ENABLE_PERFORMANCE_STATISTICS" --debug $DEBUG_ARGUMENT_VALUE $SUBDIRECTORY_OPTION $DIFF_AWARE_VALUE $SECRETS_ENABLED_VALUE $STATIC_ANALYSIS_ENABLED_VALUE || exit 1 102 | echo "Done" 103 | 104 | echo "Uploading Static Analysis Results to Datadog" 105 | datadog-ci sarif upload "$OUTPUT_FILE" --service datadog-static-analyzer --env ci || exit 1 106 | echo "Done" 107 | -------------------------------------------------------------------------------- /misc/imgs/jetbrains.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/datadog-static-analyzer/09dd4060cdc193b7075762e7f0947e283e512188/misc/imgs/jetbrains.gif -------------------------------------------------------------------------------- /misc/imgs/vscode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/datadog-static-analyzer/09dd4060cdc193b7075762e7f0947e283e512188/misc/imgs/vscode.gif -------------------------------------------------------------------------------- /misc/integration-test-classification.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This tests the `datadog-static-analyzer` binary to ensure test file classification is output correctly. 4 | # See `./helpers/test-classification.sh` for test logic. 5 | 6 | REPO_DIR=$(mktemp -d) 7 | RESULTS_FILE="${REPO_DIR}/results.json" 8 | 9 | cargo fetch 10 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 11 | 12 | ANALYSIS_CMD='cargo run --locked --profile release-dev --bin datadog-static-analyzer -- --directory "${REPO_DIR}" -o "${RESULTS_FILE}" -f sarif' 13 | 14 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 15 | "${SCRIPT_DIR}/helpers/test-classification.sh" "${ANALYSIS_CMD}" "${REPO_DIR}" "${RESULTS_FILE}" || { 16 | rm -rf "${REPO_DIR}" 17 | echo "Test failed" 18 | exit 1 19 | } 20 | 21 | rm -rf "${REPO_DIR}" 22 | 23 | echo "All tests passed" 24 | exit 0 25 | -------------------------------------------------------------------------------- /misc/integration-test-default-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This test ensures that an invocation of the analyzer fetches a default configuration (if it exists and credentials are valid). 4 | 5 | cargo fetch 6 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 7 | 8 | # (An arbitrary repository that contains at least one file with an extension supported by datadog-static-analyzer) 9 | # 10 | # Note that we override the repo name to be `datadog-static-analyzer-test-repo` (which is set up to have a repo-specific 11 | # configuration to ensure a stable test setup) 12 | REPO_DIR=$(mktemp -d) 13 | git clone https://github.com/juli1/rosie-tests.git "${REPO_DIR}" \ 14 | && git -C "${REPO_DIR}" checkout 37874bd2fcb1d39a9ce4a614e6a07826e04d0cb1 -q \ 15 | && git -C "${REPO_DIR}" remote set-url origin https://github.com/DataDog/datadog-static-analyzer-test-repo 16 | 17 | SARIF_FILENAME="results.json" 18 | 19 | rm -f "${REPO_DIR}/static-analysis.datadog.yml" 20 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/${SARIF_FILENAME}" -f sarif 21 | 22 | sarif_ruleset_count=$(jq '[.runs[0].tool.driver.rules[].id | split("/") | .[0]] | unique | length' "${REPO_DIR}/${SARIF_FILENAME}") \ 23 | || { echo "failed to parse SARIF ruleset count" >&2; exit 1; } 24 | 25 | if ! [[ "${sarif_ruleset_count}" =~ ^[0-9]+$ ]]; then 26 | echo "expected jq to output an integer for sarif_ruleset_count, got \`${sarif_ruleset_count}\`" >&2 27 | exit 1 28 | fi 29 | 30 | if [[ "${sarif_ruleset_count}" -le 1 ]]; then 31 | echo "expected at least one ruleset to have been fetched" >&2 32 | exit 1 33 | fi 34 | 35 | echo "All tests passed" 36 | exit 0 37 | -------------------------------------------------------------------------------- /misc/integration-test-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo fetch 4 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 5 | 6 | ## A Python repository 7 | echo "Checking docker repository" 8 | REPO_DIR=$(mktemp -d) 9 | export REPO_DIR 10 | git clone --depth=1 https://github.com/juli1/dd-sa-dockerfile.git "${REPO_DIR}" 11 | 12 | echo "rulesets:"> "${REPO_DIR}/static-analysis.datadog.yml" 13 | echo " - docker-best-practices" >> "${REPO_DIR}/static-analysis.datadog.yml" 14 | 15 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results.json" -f sarif 16 | 17 | if [ $? -ne 0 ]; then 18 | echo "fail to analyze docker repository" 19 | exit 1 20 | fi 21 | 22 | RES=`jq '.runs[0].results | length ' "${REPO_DIR}/results.json"` 23 | 24 | echo "Found $RES errors on first run" 25 | 26 | if [ "$RES" -lt "1" ]; then 27 | echo "test invariant: expected at least 1 violation" 28 | exit 1 29 | fi 30 | 31 | echo "All tests passed" 32 | 33 | exit 0 34 | -------------------------------------------------------------------------------- /misc/integration-test-encoding.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Test that we find errors on repositories with different encoding. 4 | ## The repository https://github.com/muh-nee/sast-files-encoding.git contains 5 | ## code with different encodings. 6 | 7 | cargo fetch 8 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 9 | 10 | ## An R repository 11 | echo "Checking muh-nee/sast-files-encoding" 12 | REPO_DIR=$(mktemp -d) 13 | export REPO_DIR 14 | git clone --depth=1 https://github.com/muh-nee/sast-files-encoding.git "${REPO_DIR}" 15 | 16 | echo "rulesets:"> "${REPO_DIR}/static-analysis.datadog.yml" 17 | echo " - python-security" >> "${REPO_DIR}/static-analysis.datadog.yml" 18 | 19 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results.json" -f sarif 20 | 21 | if [ $? -ne 0 ]; then 22 | echo "failed to analyze muh-nee/sast-files-encoding" 23 | exit 1 24 | fi 25 | 26 | RES=`jq '.runs[0].results | length ' "${REPO_DIR}/results.json"` 27 | 28 | echo "Found $RES errors" 29 | 30 | if [ "$RES" -lt "2" ]; then 31 | echo "not enough errors found" 32 | exit 1 33 | fi 34 | 35 | echo "All tests passed" 36 | 37 | exit 0 -------------------------------------------------------------------------------- /misc/integration-test-filter-rules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo fetch 4 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 5 | 6 | echo "Checking juice shop" 7 | REPO_DIR="$(mktemp -d)" 8 | UNFILTERED_OUTPUT="${REPO_DIR}/results-unfiltered.csv" 9 | FILTERED_OUTPUT="${REPO_DIR}/results-filtered.csv" 10 | EXPECTED_FILTERED_OUT='^test/server/verifySpec.ts,typescript-node-security/|^routes.login.ts,typescript-node-security/sql-injection' 11 | git clone --depth=1 https://github.com/juice-shop/juice-shop.git "${REPO_DIR}" 12 | 13 | cat << EOT > "${REPO_DIR}/static-analysis.datadog.yml" 14 | rulesets: 15 | - typescript-node-security 16 | EOT 17 | 18 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${UNFILTERED_OUTPUT}" -f csv 19 | 20 | if [ $? -ne 0 ]; then 21 | echo "failed to analyze juice-shop (without rule filters)" 22 | exit 1 23 | fi 24 | 25 | if ! grep -E -q "${EXPECTED_FILTERED_OUT}" "${UNFILTERED_OUTPUT}" ; then 26 | echo "output of no-filters run doesn't contain expected findings" 27 | exit 1 28 | fi 29 | 30 | cat << EOT > "${REPO_DIR}/static-analysis.datadog.yml" 31 | rulesets: 32 | - typescript-node-security: 33 | ignore: 34 | - "test/*/verifySpec.ts" 35 | rules: 36 | sql-injection: 37 | only: 38 | - "data/static" 39 | EOT 40 | 41 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${FILTERED_OUTPUT}" -f csv 42 | 43 | if [ $? -ne 0 ]; then 44 | echo "failed to analyze juice-shop (with rule filters)" 45 | exit 1 46 | fi 47 | 48 | if grep -E -q "${EXPECTED_FILTERED_OUT}" "${FILTERED_OUTPUT}" ; then 49 | echo "output of run with filters contains findings that should have been excluded" 50 | exit 1 51 | fi 52 | 53 | echo "All tests passed" 54 | 55 | exit 0 56 | -------------------------------------------------------------------------------- /misc/integration-test-git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This integration test for datadog-static-analyzer 4 | # 1. Check out a basic repo 5 | # 2. Run the latest version of the analyzer on it 6 | # 3. Check that we get the SHA of the commit and the category in the output SARIF file. 7 | 8 | cargo fetch 9 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 10 | 11 | ## First, test a repository to check that the commit that indicates the repo information for a violation 12 | echo "Checking rosie tests" 13 | REPO_DIR=$(mktemp -d) 14 | export REPO_DIR 15 | git clone https://github.com/juli1/rosie-tests.git "${REPO_DIR}" \ 16 | && git -C "${REPO_DIR}" checkout 37874bd2fcb1d39a9ce4a614e6a07826e04d0cb1 -q 17 | 18 | echo "rulesets:"> "${REPO_DIR}/static-analysis.datadog.yml" 19 | echo " - python-security" >> "${REPO_DIR}/static-analysis.datadog.yml" 20 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results.json" -f sarif -g 21 | if [ $? -ne 0 ]; then 22 | echo "fail to analyze rosie-tests" 23 | exit 1 24 | fi 25 | 26 | # Getting the SHA of the violation detected by the rule python-security/subprocess-shell-true 27 | FIRST_SHA=$(jq '.runs[0].results[] | select( .ruleId | contains("python-security/subprocess-shell-true")).partialFingerprints["SHA"]' "${REPO_DIR}/results.json") 28 | 29 | # Getting the SHA of the violation detected by the rule python-security/yaml-load 30 | SECOND_SHA=$(jq '.runs[0].results[] | select( .ruleId | contains("python-security/yaml-load")).partialFingerprints["SHA"]' "${REPO_DIR}/results.json") 31 | 32 | if [ "${FIRST_SHA}" != "\"5509900dc490cedbe2bb64afaf43478e24ad144b\"" ]; then 33 | echo "invalid first SHA ${FIRST_SHA}" 34 | exit 1 35 | fi 36 | 37 | 38 | if [ "${SECOND_SHA}" != "\"8c5080ff058d5d34961b9941ef498fc238be1caf\"" ]; then 39 | echo "invalid second SHA ${SECOND_SHA}" 40 | exit 1 41 | fi 42 | 43 | echo "All tests passed" 44 | 45 | exit 0 46 | -------------------------------------------------------------------------------- /misc/integration-test-js-ts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo fetch 4 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 5 | 6 | echo "Checking juice shop" 7 | REPO_DIR=$(mktemp -d) 8 | export REPO_DIR 9 | git clone --depth=1 https://github.com/juice-shop/juice-shop.git "${REPO_DIR}" 10 | echo "rulesets:"> "${REPO_DIR}/static-analysis.datadog.yml" 11 | echo " - javascript-best-practices" >> "${REPO_DIR}/static-analysis.datadog.yml" 12 | echo " - typescript-best-practices" >> "${REPO_DIR}/static-analysis.datadog.yml" 13 | echo " - javascript-common-security" >> "${REPO_DIR}/static-analysis.datadog.yml" 14 | echo " - typescript-common-security" >> "${REPO_DIR}/static-analysis.datadog.yml" 15 | echo " - javascript-inclusive" >> "${REPO_DIR}/static-analysis.datadog.yml" 16 | echo " - typescript-inclusive" >> "${REPO_DIR}/static-analysis.datadog.yml" 17 | echo " - javascript-code-style" >> "${REPO_DIR}/static-analysis.datadog.yml" 18 | echo " - jsx-react" >> "${REPO_DIR}/static-analysis.datadog.yml" 19 | echo " - tsx-react" >> "${REPO_DIR}/static-analysis.datadog.yml" 20 | echo " - javascript-node-security" >> "${REPO_DIR}/static-analysis.datadog.yml" 21 | echo " - typescript-node-security" >> "${REPO_DIR}/static-analysis.datadog.yml" 22 | 23 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results.json" -f sarif 24 | 25 | if [ $? -ne 0 ]; then 26 | echo "fail to analyze juice-shop" 27 | exit 1 28 | fi 29 | 30 | FINDINGS=`jq '.runs[0].results|length' ${REPO_DIR}/results.json` 31 | echo "Found $FINDINGS violations" 32 | if [ $FINDINGS -lt 1 ]; then 33 | echo "test invariant: expected at least 1 violation" 34 | exit 1 35 | fi 36 | 37 | echo "All tests passed" 38 | 39 | exit 0 40 | -------------------------------------------------------------------------------- /misc/integration-test-python.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo fetch 4 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 5 | 6 | ## A Python repository 7 | echo "Checking django repository" 8 | REPO_DIR=$(mktemp -d) 9 | export REPO_DIR 10 | git clone --depth=1 https://github.com/gothinkster/django-realworld-example-app.git "${REPO_DIR}" 11 | 12 | 13 | echo "rulesets:"> "${REPO_DIR}/static-analysis.datadog.yml" 14 | echo " - python-security" >> "${REPO_DIR}/static-analysis.datadog.yml" 15 | echo " - python-best-practices" >> "${REPO_DIR}/static-analysis.datadog.yml" 16 | echo " - python-django" >> "${REPO_DIR}/static-analysis.datadog.yml" 17 | echo " - python-inclusive" >> "${REPO_DIR}/static-analysis.datadog.yml" 18 | 19 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results.json" -f sarif 20 | 21 | cat "${REPO_DIR}/results.json" 22 | 23 | if [ $? -ne 0 ]; then 24 | echo "fail to analyze django repository" 25 | exit 1 26 | fi 27 | 28 | RES=`jq '.runs[0].results | length ' "${REPO_DIR}/results.json"` 29 | 30 | echo "Found $RES errors" 31 | 32 | if [ "$RES" -lt "1" ]; then 33 | echo "test invariant: expected at least 1 violation" 34 | exit 1 35 | fi 36 | 37 | # Test that --fail-on-any-violation returns a non-zero return code 38 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results.json" -f sarif --fail-on-any-violation=none,notice,warning,error 39 | 40 | if [ $? -eq 0 ]; then 41 | echo "static analyzer reports 0 when it should not" 42 | exit 1 43 | fi 44 | 45 | echo "All tests passed" 46 | 47 | exit 0 48 | -------------------------------------------------------------------------------- /misc/integration-test-r.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo fetch 4 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 5 | 6 | ## An R repository 7 | echo "Checking stuart-lab/signac" 8 | REPO_DIR=$(mktemp -d) 9 | export REPO_DIR 10 | git clone --depth=1 https://github.com/stuart-lab/signac.git "${REPO_DIR}" 11 | 12 | echo "rulesets:"> "${REPO_DIR}/static-analysis.datadog.yml" 13 | echo " - r-code-style" >> "${REPO_DIR}/static-analysis.datadog.yml" 14 | echo " - r-inclusive" >> "${REPO_DIR}/static-analysis.datadog.yml" 15 | 16 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results2.json" -f sarif 17 | 18 | if [ $? -ne 0 ]; then 19 | echo "failed to analyze stuart-lab/signac" 20 | exit 1 21 | fi 22 | 23 | RES=`jq '.runs[0].results | length ' "${REPO_DIR}/results2.json"` 24 | 25 | echo "Found $RES errors on second run" 26 | 27 | if [ "$RES" -lt "1" ]; then 28 | echo "test invariant: expected at least 1 violation" 29 | exit 1 30 | fi 31 | 32 | # Test that --fail-on-any-violation returns a non-zero return code 33 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results2.json" -f sarif --fail-on-any-violation=none,notice,warning,error 34 | 35 | if [ $? -eq 0 ]; then 36 | echo "static analyzer reports 0 when it should not" 37 | exit 1 38 | fi 39 | 40 | echo "All tests passed" 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /misc/integration-test-rust.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo fetch 4 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 5 | 6 | ## An R repository 7 | echo "Checking tokio-rs/tokio" 8 | REPO_DIR=$(mktemp -d) 9 | export REPO_DIR 10 | git clone --depth=1 https://github.com/tokio-rs/tokio.git "${REPO_DIR}" 11 | 12 | echo "rulesets:"> "${REPO_DIR}/static-analysis.datadog.yml" 13 | echo " - rust-code-style" >> "${REPO_DIR}/static-analysis.datadog.yml" 14 | echo " - rust-inclusive" >> "${REPO_DIR}/static-analysis.datadog.yml" 15 | 16 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results2.json" -f sarif 17 | 18 | if [ $? -ne 0 ]; then 19 | echo "failed to analyze tokio-rs/tokio" 20 | exit 1 21 | fi 22 | 23 | RES=`jq '.runs[0].results | length ' "${REPO_DIR}/results2.json"` 24 | 25 | echo "Found $RES errors on second run" 26 | 27 | if [ "$RES" -lt "2" ]; then 28 | echo "not enough errors found" 29 | exit 1 30 | fi 31 | 32 | # Test that --fail-on-any-violation returns a non-zero return code 33 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results2.json" -f sarif --fail-on-any-violation=none,notice,warning,error 34 | 35 | if [ $? -eq 0 ]; then 36 | echo "static analyzer reports 0 when it should not" 37 | exit 1 38 | fi 39 | 40 | echo "All tests passed" 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /misc/integration-test-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo fetch 4 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 5 | 6 | ## A Python repository 7 | echo "Checking secrets-tests repository" 8 | REPO_DIR=$(mktemp -d) 9 | export REPO_DIR 10 | git clone --depth=1 https://github.com/muh-nee/secrets-tests.git "${REPO_DIR}" 11 | 12 | # Test without the static-analysis.datadog.yml file 13 | rm -f "${REPO_DIR}/static-analysis.datadog.yml" 14 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results1.json" -f sarif --enable-secrets true --enable-static-analysis false 15 | 16 | if [ $? -ne 0 ]; then 17 | echo "fail to analyze secrets-tests repository" 18 | exit 1 19 | fi 20 | 21 | RES=`jq '.runs[0].results | length ' "${REPO_DIR}/results1.json"` 22 | 23 | echo "Found $RES errors on first run" 24 | 25 | EXPECTING=5 26 | 27 | if [ "$RES" -ne "$EXPECTING" ]; then 28 | echo "incorrect number of errors found, found $RES, expected $EXPECTING" 29 | exit 1 30 | fi 31 | 32 | status1=`jq '.runs[0].results[0].properties.tags[1]' "${REPO_DIR}/results1.json"` 33 | 34 | if [ "$status1" != "\"DATADOG_SECRET_VALIDATION_STATUS:INVALID\"" ]; then 35 | echo "status1: did not find DATADOG_SECRET_VALIDATION_STATUS:INVALID in properties, found $status1" 36 | exit 1 37 | fi 38 | 39 | status2=`jq '.runs[0].results[1].properties.tags[1]' "${REPO_DIR}/results1.json"` 40 | 41 | if [ "$status2" != "\"DATADOG_SECRET_VALIDATION_STATUS:NOT_VALIDATED\"" ]; then 42 | echo "status2: did not find DATADOG_SECRET_VALIDATION_STATUS:NOT_VALIDATED in properties, found $status2" 43 | exit 1 44 | fi 45 | 46 | status3=`jq '.runs[0].results[2].properties.tags[1]' "${REPO_DIR}/results1.json"` 47 | 48 | if [ "$status3" != "\"DATADOG_SECRET_VALIDATION_STATUS:NOT_VALIDATED\"" ]; then 49 | echo "status3: did not find DATADOG_SECRET_VALIDATION_STATUS:NOT_VALIDATED in properties, found $status3" 50 | exit 1 51 | fi 52 | 53 | ## Make sure the SDS ID is added to the SARIF file 54 | status4=`jq '.runs[0].tool.driver.rules[0].properties.tags[1]' "${REPO_DIR}/results1.json"` 55 | 56 | if [ "$status4" != "\"DATADOG_SDS_ID:5ef83eb0-4137-48a2-a28b-2402c185863c\"" ]; then 57 | echo "status3: did not find DATADOG_SDS_ID:5ef83eb0-4137-48a2-a28b-2402c185863c in properties, found $status4" 58 | exit 1 59 | fi 60 | 61 | echo "All tests passed" 62 | 63 | exit 0 -------------------------------------------------------------------------------- /misc/integration-test-sql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo fetch 4 | cargo build --locked --profile release-dev --bin datadog-static-analyzer 5 | 6 | ## An R repository 7 | echo "Checking cisagov/cset" 8 | REPO_DIR=$(mktemp -d) 9 | export REPO_DIR 10 | git clone --depth=1 https://github.com/SparkhoundSQL/sql-server-toolbox.git "${REPO_DIR}" 11 | 12 | echo "rulesets:"> "${REPO_DIR}/static-analysis.datadog.yml" 13 | echo " - sql-code-style" >> "${REPO_DIR}/static-analysis.datadog.yml" 14 | echo " - sql-inclusive" >> "${REPO_DIR}/static-analysis.datadog.yml" 15 | 16 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results2.json" -f sarif 17 | 18 | if [ $? -ne 0 ]; then 19 | echo "failed to analyze cisagov/cset" 20 | exit 1 21 | fi 22 | 23 | RES=`jq '.runs[0].results | length ' "${REPO_DIR}/results2.json"` 24 | 25 | echo "Found $RES errors on second run" 26 | 27 | if [ "$RES" -lt "1" ]; then 28 | echo "test invariant: expected at least 1 violation" 29 | exit 1 30 | fi 31 | 32 | # Test that --fail-on-any-violation returns a non-zero return code 33 | ./target/release-dev/datadog-static-analyzer --directory "${REPO_DIR}" -o "${REPO_DIR}/results2.json" -f sarif --fail-on-any-violation=none,notice,warning,error 34 | 35 | if [ $? -eq 0 ]; then 36 | echo "static analyzer reports 0 when it should not" 37 | exit 1 38 | fi 39 | 40 | echo "All tests passed" 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.86" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /schema/Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | test: test-valid test-invalid 4 | 5 | INVALID_FILES=examples/invalid/*.yml 6 | VALID_FILES=examples/valid/*.yml 7 | 8 | test-valid: 9 | pajv test -s schema.json -d "${VALID_FILES}" --valid 10 | 11 | test-invalid: 12 | pajv test -s schema.json -d "${INVALID_FILES}" --invalid 13 | 14 | .PHONY: test test-valid test-invalid 15 | -------------------------------------------------------------------------------- /schema/README.md: -------------------------------------------------------------------------------- 1 | # Schema for datadog-static-analyzer 2 | 3 | 1. It validates what objects are required and valid 4 | 2. Adding more properties in the schema is still making them valid 5 | 3. Some validation may be missing but the core idea is present 6 | 7 | ## How to test 8 | 9 | 1. Install https://www.npmjs.com/package/pajv (`npm install -g pajv`) 10 | 2. Invoke `make` 11 | 12 | 13 | ## Configuration file examples 14 | 15 | - [valid files here](examples/valid) 16 | - [invalid files here](examples/invalid) 17 | -------------------------------------------------------------------------------- /schema/examples/invalid/disable-ignore-generated-files.yml: -------------------------------------------------------------------------------- 1 | schema-version: v1 2 | rulesets: 3 | - python-best-practices 4 | - go-best-practices: 5 | - java-best-practices: 6 | rules: 7 | avoid-printstacktrace: 8 | only: 9 | - "foo/bar" 10 | ignore: 11 | - path1/path2 12 | only: 13 | - path2/path3 14 | ignore-generated-files: "wefwe" -------------------------------------------------------------------------------- /schema/examples/invalid/gitignore-wrong-type.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - python-best-practices 3 | - java-best-practices 4 | - python-security 5 | # 'ignore-gitignore' is a boolean 6 | ignore-gitignore: 42 7 | -------------------------------------------------------------------------------- /schema/examples/invalid/ignore-wrong-type.yml: -------------------------------------------------------------------------------- 1 | rulesets: [python-style] 2 | # 'ignore' must be a list. 3 | ignore: foo -------------------------------------------------------------------------------- /schema/examples/invalid/invalid-version.yml: -------------------------------------------------------------------------------- 1 | schema-version: v0 2 | rulesets: 3 | - python-best-practices 4 | - java-best-practices 5 | - python-security 6 | -------------------------------------------------------------------------------- /schema/examples/invalid/maxfilesize-wrong-type.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - python-best-practices 3 | - java-best-practices 4 | - python-security 5 | # 'max-file-size-kb' is a number 6 | max-file-size-kb: "100" 7 | -------------------------------------------------------------------------------- /schema/examples/invalid/only-wrong-type.yml: -------------------------------------------------------------------------------- 1 | rulesets: [python-style] 2 | # 'only' must be a list. 3 | only: foo -------------------------------------------------------------------------------- /schema/examples/invalid/rules-arguments-wrong-type-list.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | rules: 4 | avoid-printstacktrace: 5 | # 'rulesets/rules//arguments' is a map. 6 | arguments: 7 | - foo 8 | - bar 9 | -------------------------------------------------------------------------------- /schema/examples/invalid/rules-arguments-wrong-type-map-of-list.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | rules: 4 | avoid-printstacktrace: 5 | arguments: 6 | # 'rulesets/rules//arguments/' is a string or a map. 7 | foo: 8 | - bar 9 | -------------------------------------------------------------------------------- /schema/examples/invalid/rules-arguments-wrong-type-string.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | rules: 4 | avoid-printstacktrace: 5 | # 'rulesets/rules//arguments' is a map. 6 | arguments: foo 7 | -------------------------------------------------------------------------------- /schema/examples/invalid/rules-category-invalid-value.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | rules: 4 | avoid-printstacktrace: 5 | category: XXXXXX 6 | -------------------------------------------------------------------------------- /schema/examples/invalid/rules-ignore-wrong-type.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | rules: 4 | avoid-printstacktrace: 5 | # 'rulesets/rules//ignore' is a list. 6 | ignore: foo 7 | -------------------------------------------------------------------------------- /schema/examples/invalid/rules-only-wrong-type.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | rules: 4 | avoid-printstacktrace: 5 | # 'rulesets/rules//ignore' is a list. 6 | only: foo 7 | -------------------------------------------------------------------------------- /schema/examples/invalid/rules-severity-invalid-value.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | rules: 4 | avoid-printstacktrace: 5 | severity: XXXXXX 6 | -------------------------------------------------------------------------------- /schema/examples/invalid/rules-wrong-type-list.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | # 'rulesets/rules' is not a list. 4 | rules: 5 | - avoid-printstacktrace 6 | - loose-coupling 7 | - one-declaration-per-line 8 | -------------------------------------------------------------------------------- /schema/examples/invalid/rules-wrong-type-string.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | # 'rulesets/rules' is not a string 4 | rules: avoid-printstacktrace 5 | -------------------------------------------------------------------------------- /schema/examples/invalid/rulesets-absent.yml: -------------------------------------------------------------------------------- 1 | # 'rulesets' must be present. 2 | ignore: 3 | - path1/path2 4 | only: 5 | - path2/path3 6 | -------------------------------------------------------------------------------- /schema/examples/invalid/rulesets-empty.yml: -------------------------------------------------------------------------------- 1 | # 'rulesets' must have elements. 2 | rulesets: [ ] 3 | ignore: 4 | - path1/path2 5 | only: 6 | - path2/path3 7 | -------------------------------------------------------------------------------- /schema/examples/invalid/rulesets-ignore-wrong-type.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - go-best-practices: 3 | # 'rulesets/ignore' must be a list 4 | ignore: bar 5 | -------------------------------------------------------------------------------- /schema/examples/invalid/rulesets-indentation.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | # The configuration for each ruleset must be aligned below the ruleset's name. 3 | - java-best-practices: 4 | only: [foo] 5 | - go-best-practices: 6 | ignore: [bar] 7 | -------------------------------------------------------------------------------- /schema/examples/invalid/rulesets-only-wrong-type.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - java-best-practices: 3 | # 'rulesets/only' must be a list 4 | only: foo 5 | -------------------------------------------------------------------------------- /schema/examples/invalid/rulesets-wrong-type-map.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | # 'rulesets' must be a list. 3 | java-best-practices: 4 | only: [foo] 5 | go-best-practices: 6 | ignore: [bar] 7 | -------------------------------------------------------------------------------- /schema/examples/valid/arguments.yml: -------------------------------------------------------------------------------- 1 | schema-version: v1 2 | rulesets: 3 | - lorem_ipsum: 4 | ignore: 5 | - one/two 6 | rules: 7 | dolor: 8 | ignore: [one/two] 9 | only: 10 | - one/two/three 11 | - one/two/four 12 | arguments: 13 | sit_amet: "20" 14 | consectetur: 15 | /: 40 16 | one/two/three/four: "80" 17 | one/two/three/four/five: "160" 18 | -------------------------------------------------------------------------------- /schema/examples/valid/complex1.yml: -------------------------------------------------------------------------------- 1 | schema-version: v1 2 | rulesets: 3 | - python-best-practices 4 | - go-best-practices: 5 | - java-best-practices: 6 | rules: 7 | avoid-printstacktrace: 8 | only: 9 | - "foo/bar" 10 | ignore: 11 | - path1/path2 12 | only: 13 | - path2/path3 14 | -------------------------------------------------------------------------------- /schema/examples/valid/complex2.yml: -------------------------------------------------------------------------------- 1 | schema-version: v1 2 | rulesets: 3 | - python-best-practices 4 | - java-best-practices: 5 | rules: 6 | avoid-printstacktrace: 7 | only: 8 | - "foo/bar" 9 | arguments: 10 | foo: "bar" 11 | bar: 12 | /: 42 13 | uno/dos: "32" 14 | loose-coupling: 15 | arguments: 16 | foo: bar 17 | one-declaration-per-line: 18 | severity: ERROR 19 | category: CODE_STYLE 20 | ignore: 21 | - "**" 22 | - go-best-practices: 23 | ignore: 24 | - path1/path2 25 | only: 26 | - path2/path3 27 | -------------------------------------------------------------------------------- /schema/examples/valid/disable-ignore-generated-files.yml: -------------------------------------------------------------------------------- 1 | schema-version: v1 2 | rulesets: 3 | - python-best-practices 4 | - go-best-practices: 5 | - java-best-practices: 6 | rules: 7 | avoid-printstacktrace: 8 | only: 9 | - "foo/bar" 10 | ignore: 11 | - path1/path2 12 | only: 13 | - path2/path3 14 | ignore-generated-files: false -------------------------------------------------------------------------------- /schema/examples/valid/extensions.yml: -------------------------------------------------------------------------------- 1 | # Additional fields not handled by the schema are accepted. 2 | # (Additional field names do NOT have to start with x-. This is done for tests only.) 3 | schema-version: v1 4 | rulesets: 5 | - java-best-practices: 6 | x-ruleset-field: abc 7 | ignore: [foo] 8 | only: [bar] 9 | rules: 10 | avoid-printstacktrace: 11 | x-rule-field: abc 12 | only: [foo/bar] 13 | ignore: [foo/baz] 14 | arguments: 15 | foo: "bar" 16 | - go-best-practices: 17 | x-other-ruleset-field: abc 18 | ignore: 19 | - path1/path2 20 | only: 21 | - path2/path3 22 | x-root-field: abc 23 | -------------------------------------------------------------------------------- /schema/examples/valid/severity.yml: -------------------------------------------------------------------------------- 1 | schema-version: v1 2 | rulesets: 3 | - java-best-practices: 4 | rules: 5 | avoid-printstacktrace: 6 | severity: ERROR 7 | one-declaration-per-line: 8 | severity: 9 | /: WARNING 10 | uno/dos: ERROR 11 | elsewhere: NOTICE 12 | loose-coupling: 13 | tres: NOTICE 14 | tres/cuatro: ERROR 15 | -------------------------------------------------------------------------------- /schema/examples/valid/simple.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - python-best-practices 3 | - java-best-practices 4 | - python-security 5 | -------------------------------------------------------------------------------- /schema/examples/valid/with-gitignore.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - python-best-practices 3 | - java-best-practices 4 | - python-security 5 | ignore-gitignore: false 6 | -------------------------------------------------------------------------------- /schema/examples/valid/with-maxfilesize.yml: -------------------------------------------------------------------------------- 1 | rulesets: 2 | - python-best-practices 3 | - java-best-practices 4 | - python-security 5 | max-file-size-kb: 100 6 | -------------------------------------------------------------------------------- /schema/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "schema-version": { 6 | "type": "string", 7 | "default": "v1", 8 | "enum": ["v1"] 9 | }, 10 | "rulesets": { 11 | "type": "array", 12 | "items": { 13 | "anyOf": [ 14 | { 15 | "$ref": "#/definitions/ruleset" 16 | }, 17 | { 18 | "$ref": "#/definitions/emptyRuleset" 19 | }, 20 | { 21 | "type": "string", 22 | "minLength": 1 23 | } 24 | ] 25 | }, 26 | "minItems": 1 27 | }, 28 | "ignore": { 29 | "$ref": "#/definitions/pathList" 30 | }, 31 | "only": { 32 | "$ref": "#/definitions/pathList" 33 | }, 34 | "ignore-gitignore": { 35 | "type": "boolean" 36 | }, 37 | "ignore-generated-files": { 38 | "type": "boolean" 39 | }, 40 | "max-file-size-kb": { 41 | "type": "number" 42 | } 43 | }, 44 | "required": [ 45 | "rulesets" 46 | ], 47 | "definitions": { 48 | "rule": { 49 | "type": "object", 50 | "properties": { 51 | "ignore": { 52 | "$ref": "#/definitions/pathList" 53 | }, 54 | "only": { 55 | "$ref": "#/definitions/pathList" 56 | }, 57 | "arguments": { 58 | "type": "object", 59 | "additionalProperties": { 60 | "$ref": "#/definitions/argumentValue" 61 | } 62 | }, 63 | "severity": { 64 | "anyOf": [ 65 | { 66 | "$ref": "#/definitions/singularSeverityValue" 67 | }, 68 | { 69 | "type": "object", 70 | "additionalProperties": { 71 | "$ref": "#/definitions/singularSeverityValue" 72 | } 73 | } 74 | ] 75 | }, 76 | "category": { 77 | "enum": [ 78 | "BEST_PRACTICES", 79 | "CODE_STYLE", 80 | "ERROR_PRONE", 81 | "PERFORMANCE", 82 | "SECURITY" 83 | ] 84 | } 85 | } 86 | }, 87 | "emptyRuleset": { 88 | "type": "object", 89 | "additionalProperties": { 90 | "type": "null" 91 | }, 92 | "minProperties": 1, 93 | "maxProperties": 1 94 | }, 95 | "ruleset": { 96 | "type": "object", 97 | "properties": { 98 | "rules": { 99 | "type": "object", 100 | "additionalProperties": { 101 | "$ref": "#/definitions/rule" 102 | } 103 | }, 104 | "ignore": { 105 | "$ref": "#/definitions/pathList" 106 | }, 107 | "only": { 108 | "$ref": "#/definitions/pathList" 109 | } 110 | }, 111 | "minProperties": 2 112 | }, 113 | "pathList": { 114 | "type": "array", 115 | "items": { 116 | "type": "string", 117 | "minLength": 1 118 | } 119 | }, 120 | "argumentValue": { 121 | "anyOf": [ 122 | { 123 | "$ref": "#/definitions/singularArgumentValue" 124 | }, 125 | { 126 | "type": "object", 127 | "additionalProperties": { 128 | "$ref": "#/definitions/singularArgumentValue" 129 | } 130 | } 131 | ] 132 | }, 133 | "singularArgumentValue": { 134 | "anyOf": [ 135 | { 136 | "type": "string" 137 | }, 138 | { 139 | "type": "number", 140 | "$comment": "will be internally coerced to string" 141 | }, 142 | { 143 | "type": "boolean", 144 | "$comment": "will be internally coerced to string" 145 | } 146 | ] 147 | }, 148 | "singularSeverityValue": { 149 | "enum": [ 150 | "ERROR", 151 | "WARNING", 152 | "NOTICE", 153 | "NONE" 154 | ] 155 | } 156 | } 157 | } 158 | 159 | 160 | --------------------------------------------------------------------------------