diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d5a0f8..283e629 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -434,6 +434,65 @@ jobs: echo "$out" | grep -q 'PROVEN repo SCA scan' || { echo "::error::the repo SCA link is not PROVEN in a job that installs Trivy and its database"; exit 1; } + # Lane B (Phase 6) with the real, pinned recall tools. The Go job runs the + # same chain with the tools' recorded reports replayed + # (internal/recall/recalltest); this job is what proves the recording is + # faithful, and that the built binary scans the planted fixture end to end. + laneb-e2e: + name: Lane B end to end (real tools) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install the pinned recall tools + run: | + set -euo pipefail + # Pinned by version and digest, the values in + # eval/tools/opengrep/MANIFEST.toml and data/rules/selection.json. + OPENGREP_SHA256=40c21299eeddabf743b856daa843d24f9d4a027130671cd45b3b21776fd9ab26 + curl -sSfL -o opengrep \ + "https://github.com/opengrep/opengrep/releases/download/v1.26.0/opengrep_manylinux_x86" + echo "${OPENGREP_SHA256} opengrep" | sha256sum -c - + sudo install -m 0755 opengrep /usr/local/bin/opengrep + GOSEC_SHA256=6431b119741c1f4a50fdfcf94e782e16b9e642afc8c7fa9b5d39d48bf3003095 + curl -sSfL -o gosec.tar.gz \ + "https://github.com/securego/gosec/releases/download/v2.29.0/gosec_2.29.0_linux_amd64.tar.gz" + echo "${GOSEC_SHA256} gosec.tar.gz" | sha256sum -c - + tar -xzf gosec.tar.gz gosec + sudo install -m 0755 gosec /usr/local/bin/gosec + python -m pip install bandit==1.9.4 + python -m pip install -r eval/requirements.txt + opengrep --version && gosec -version && bandit --version + + - name: The opt-in is set, so a missing tool must FAIL rather than refuse quietly + env: + ANVIL_LANEB_E2E: "1" + run: go test -count=1 -v -run TestTheFixtureWithTheRealTools ./internal/recall/ + + - name: Lane B end to end on the fixture, through the built binary + run: sh test/e2e/laneb.sh + + - name: Negative control -- the job must fail if opengrep disappears + env: + ANVIL_LANEB_E2E: "1" + run: | + set -uo pipefail + sudo mv /usr/local/bin/opengrep /usr/local/bin/opengrep.hidden + if go test -count=1 -run TestTheFixtureWithTheRealTools ./internal/recall/; then + sudo mv /usr/local/bin/opengrep.hidden /usr/local/bin/opengrep + echo "::error::The Lane B E2E test PASSED with no opengrep present. The opt-in is not fatal, so this job proves nothing." + exit 1 + fi + sudo mv /usr/local/bin/opengrep.hidden /usr/local/bin/opengrep + echo "confirmed: with ANVIL_LANEB_E2E=1 and no opengrep, the test fails" + # The spine's Go control-plane decision permits Python in exactly three places, and the # evaluation harness is one of them. eval/ is a pure-Python tree. # diff --git a/NOTICE b/NOTICE index 4ca38fd..e0a400b 100644 --- a/NOTICE +++ b/NOTICE @@ -57,6 +57,53 @@ in THIRD-PARTY-LICENSES.md with its full attribution text. github.com/mattn/go-isatty v0.0.24 MIT github.com/ncruces/go-strftime v1.0.0 MIT +================================================================================ +1a. REDISTRIBUTED IN SOURCE FORM TODAY (Lane B's vendored rules, data/rules) +================================================================================ + +Lane B's rule pack is committed under data/rules, the owner's selection of +2026-10-03. Every rule file is copied byte for byte, keeping its own header, +and its licence was read from that header or, where a corpus has no per-file +headers, from the corpus LICENSE body. Each licence body is archived beside the +rules and hashed in data/rules/MANIFEST.json; internal/recall refuses a pack +whose bodies or rules do not match. + + GitLab sast-rules (gitlab-org/security-products/sast-rules) at + 53bf5cf6df3c51b6c02110f5a638b5e6213666cd + A rule's licence is read from the stricter of its own "License:" header + and the header of its GitLab companion test file, which names the + analyser GitLab ported it from. Rules whose companion names an LGPL-3.0 + analyser (find-sec-bugs, security-code-scan: 131 rules) are not vendored; + MANIFEST.json lists each. + 52 rule files -- MIT, Copyright (c) 2011-present GitLab Inc. The MIT + notice is in data/rules/gitlab-sast-rules/LICENSE. + 6 rule files (javascript/) whose companions read "MIT (c) JS Foundation + and other contributors, https://js.foundation" -- MIT, Copyright JS + Foundation and other contributors; the MIT permission notice is the one + in data/rules/gitlab-sast-rules/LICENSE. + 27 rule files (go/), derived from gosec -- Apache-2.0, (c) gosec. + 52 rule files (python/), whose companions read "Apache 2.0 (c) PyCQA", + derived from bandit -- Apache-2.0. bandit's plugin sources carry + "Copyright 2014 Hewlett-Packard Development Company, L.P." (15 files) + and other contributors' notices (read from bandit 1.9.4, 2026-10-03); + GitLab's companions credit PyCQA. + For both Apache-2.0 groups: Section 4(a), a copy of the licence is at + data/rules/gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt (gosec's + LICENSE.txt from its 2.29.0 release archive) and + LICENSE.bandit-Apache-2.0.txt (bandit 1.9.4's licence body from its + wheel). Section 4(c): the files are copied unmodified with their own + headers. Section 4(d) applies only if the Work carries a NOTICE file; + the gosec 2.29.0 release archive and the bandit 1.9.4 wheel carry none + (checked 2026-10-03). GitLab's LICENSE puts doc/ under CC BY-SA 4.0 and + keeps third-party components under their own licences, so only the six + directories csharp, go, java, javascript, python and scala are read, and + a test refuses any other. + + 0xdea semgrep-rules (0xdea/semgrep-rules) at + dae50da6e6e629750f1daec73a01ef9206a372ab + 39 rule files, rules/c, no per-file headers -- MIT, Copyright (c) 2022 + raptor. The MIT notice is in data/rules/0xdea-semgrep-rules/LICENSE. + ================================================================================ 2. APACHE-2.0 ARTIFACTS WITH SECTION 4(d) NOTICE DUTIES ================================================================================ @@ -85,6 +132,11 @@ duty is recorded now so that the packaging steps cannot ship without it. AFL++ is excluded by the spine's exclusion list because its LICENSE is AGPL-3.0 regardless of what its README says. + gosec (securego/gosec) 2.29.0 -- Apache-2.0, read from LICENSE.txt in the + pinned release archive. bandit (PyCQA/bandit) 1.9.4 -- Apache-2.0, read + from its LICENSE body at the tag. Lane B runs both as subprocesses. + Duty: Section 4 on redistribution of either. Status: PENDING (packaging). + Docker Engine, Docker Compose -- Apache-2.0 Duty: Section 4(d) notice IF redistributed as binaries. Anvil invokes them; it does not currently redistribute them. Status: NOT TRIGGERED. @@ -198,7 +250,13 @@ documented reason, and must never appear in this file or in any Anvil artifact: irrelevant to it. Semgrep-maintained rules internal business use only. opengrep/opengrep-rules archived, NOASSERTION, LGPL-2.1 + Commons Clause. - Use AikidoSec/opengrep-rules instead (MIT). + Lane B uses GitLab's sast-rules and 0xdea's + rules instead (data/rules). + GitLab sast-rules c/ every rule headed "License: GPL 2.0", generated + from flawfinder. Excluded by the owner on + 2026-10-03. + lambdasec/autogrep Apache-2.0 on the repository, with GPL, AGPL + and unlicensed rules inside. CIS Benchmark content in any form, including rules written by reading one. Gemma 1-3, Llama, StarCoder2, diff --git a/README.md b/README.md index 041cba9..695e16c 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,16 @@ An open-source, **profit-free**, self-hostable system that finds vulnerabilities code repositories and proposes fixes — using locally-served open-weight models, and never browsing the live web at inference time. -**Status (2026-10-03): Phases 0–5 of 10 are done; Phase 6, Lane B, is next.** Phase 5's evaluation decided the -model tier: the small detection model did not earn its place (`eval/register.yaml`), so the owner shrank it to -recall rules plus an optional ranker, and the record contract is frozen at v1. `anvil scan` runs Lane A end to end: it matches a repository (through Trivy) or a host inventory -(through Anvil's own comparator) against the advisory cache, seals the SAST half, writes the record and its -findings to the store, marks each finding new, persisting, regressed or fixed against earlier scans, and writes -the record as SARIF 2.1.0. What is not built yet: Lane B's recall tier, remediation, -the dynamic tier running live (it is built but has never touched a kernel or a target), and the release packages. +**Status (2026-10-03): Phases 0–6 of 10 are built; Phase 7, remediation, is next.** Phase 5's evaluation +decided the model tier: the small detection model did not earn its place (`eval/register.yaml`), so the owner +shrank it to recall rules, and Phase 6 built Lane B that way, with no ranker (none was measured beating chance). +`anvil scan --repo` runs both lanes into one SAST half: Lane A matches dependencies through Trivy when the +operator enables its database, and Lane B runs its rule pack over the first-party source and records every +match as an `unconfirmed` finding for the coding agent's triage gate. `anvil scan --host` matches a host +inventory through Anvil's own comparator. Every scan seals the SAST half, writes the record and its findings to +the store, marks each finding new, persisting, regressed or fixed against earlier scans, and writes the record +as SARIF 2.1.0. What is not built yet: remediation and its triage gate, the dynamic tier running live (it is +built but has never touched a kernel or a target), and the release packages. A fresh clone admits no real advisory feed until an operator acquires and certifies the publishers' licence texts (`mirror/README.md`); `testdata/lanea-fixture` is an offline snapshot that proves the chain without one. @@ -26,8 +29,18 @@ export ANVIL_CONFIG=$PWD/anvil.yml ./anvil findings ``` -`sh test/e2e/fixture.sh` runs the same sequence, plus a repository scan, and asserts every exit status. Exit -statuses: 0 clean, 1 findings, 2 usage, 3 refused, 4 missing tool, 5 error; a refusal or a missing tool is never 0. +With opengrep 1.26.0, gosec 2.29.0 and bandit 1.9.4 on `PATH`, Lane B scans its planted fixture (12 unconfirmed +findings): + +```bash +printf 'version: 1\nstateDir: state\nrecall:\n rules: data/rules\n' > anvil.yml +./anvil scan --repo testdata/laneb-fixture --target laneb-fixture --out repo.sarif +./anvil recall testdata/laneb-fixture # the same candidates as JSON, nothing recorded +``` + +`sh test/e2e/fixture.sh` runs the Lane A sequence, plus a repository scan, and `sh test/e2e/laneb.sh` the Lane B +one; both assert every exit status. Exit statuses: 0 clean, 1 findings, 2 usage, 3 refused, 4 missing tool, +5 error; a refusal or a missing tool is never 0. ## What it does @@ -36,15 +49,46 @@ Two detection lanes, one audit record, and a remediation tier that proposes and - **Lane A — deterministic, zero inference.** SBOM and host-package matching by version comparator. Owns dependency and host findings. CVE/OSV/GHSA describe vulnerable *package versions*, and a version comparator answers that exactly, for free. -- **Lane B — first-party source.** A deterministic recall tier (opengrep over GitLab's sast-rules and C/C++ - rules, gosec, bandit) produces candidates, each placed on the record as `unconfirmed` for the coding agent's - triage gate to decide. The evaluation of 2026-10-03 deleted the planned small-model adjudicator: the primary - candidate flipped its verdict on a wrong advisory only about half the time and ranked vulnerable against patched - code no better than chance. A ranker ships only if it measurably beats chance on Anvil's own candidates. +- **Lane B — first-party source.** A deterministic recall tier (opengrep over GitLab's sast-rules and 0xdea's + C/C++ rules, plus gosec and bandit) produces candidates, each placed on the record as `unconfirmed` for the + coding agent's triage gate to decide. The evaluation of 2026-10-03 deleted the planned small-model + adjudicator: the primary candidate flipped its verdict on a wrong advisory only about half the time and ranked + vulnerable against patched code no better than chance. No ranker ships: none has been measured beating chance + on Anvil's own candidates, and none of them are labelled. - **Dynamic tier.** Ships as a **separate artifact** (`anvil-dast`), separately installed, requiring explicit attestation before it probes anything. - **Remediation.** Proposes patches. It does not merge them. +## What Lane B covers + +Lane B runs only the rules in `data/rules`, the owner's selection of 2026-10-03, pinned by commit and hashed +file by file, plus gosec and bandit. **No permissive rule corpus gives broad multi-language taint (dataflow) +recall**: 16 of the 176 rule files are taint rules, and Java, Scala and C# are thin: + +| Language | Rule files | Of them taint rules | Native analyser | +|---|---|---|---| +| C and C++ | 39 (0xdea, MIT) | 0 | none | +| Go | 27 (GitLab, Apache-2.0, derived from gosec) | 5 | gosec 2.29.0 (SSA, type-checked) | +| Python | 67 (GitLab: 52 Apache-2.0, derived from bandit; 15 MIT) | 1 | bandit 1.9.4 (AST) | +| Java | 12 (GitLab, MIT) | 4 | none | +| Scala | 19 (GitLab, MIT) | 6 | none | +| C# | 1 (GitLab, MIT) | 0 | none | +| JavaScript and TypeScript | 11 (GitLab, MIT) | 0 | none | + +Anything else (Ruby, PHP, Kotlin, Rust, Swift, shell, …) is not covered at all. Two kinds of GitLab rules are +excluded for their licences: the C rules, each headed "License: GPL 2.0" because it is generated from flawfinder, +and 131 Java, Scala and C# rules whose GitLab companion test files name find-sec-bugs or security-code-scan, +both LGPL-3.0, as their source (a rule's licence is read from the stricter of its own header and its +companion's), plus one Java rule whose companion states no licence at all. Test, test-data, +documentation and example trees are not reported on. **Those exclusions match by name, so a repository can place +first-party code under `spec/`, `docs/` or `fixtures/` and Lane B will not see it**; every scan reports how many +source files they kept out. Otherwise the scanned repository cannot switch a rule off: its `nosemgrep`, `#nosec` +and `# nosec` comments, `.semgrepignore` and `.bandit` files, and bandit's and opengrep's own default excludes are +all ignored, and anything a tool was given and did not analyse (another platform's Go build tags, cgo, a rule +timeout) is recorded as incomplete coverage. Measured on +2026-10-03 on five sample repositories, the selection produces 0 to 225 candidates a full scan (curl is the most; +`eval/results/candidates-per-scan.json`), under the budget of 500. Every scan records its count. + ## Three rules that are enforced in code, not documentation 1. **Never auto-merge a patch.** The best measured security-patch rate on real CVEs is 34.0%. @@ -56,7 +100,7 @@ Two detection lanes, one audit record, and a remediation tier that proposes and | Artifact | Contains | |---|---| -| `anvil` | Lane A, Lane B, record, store, remediation. **No network-probing capability compiled in.** | +| `anvil` | Lane A, Lane B, record, store, remediation. **No network-probing capability compiled in.** Lane B's tools run as separate processes; the operator installs them (opengrep 1.26.0, gosec 2.29.0, bandit 1.9.4), and Anvil checks their versions. | | `anvil-dast` | The dynamic tier. Separate release, separate install, explicit attestation. | This is a split in the build, not a configuration flag, because a boolean inside a single shipped @@ -90,6 +134,10 @@ Advisory data reaches the cache only through the licence gate: a feed is admitte text has been acquired, read, and pinned by digest in `mirror/LICENSE-MANIFEST.toml`, and share-alike sources are quarantined in tier 2. +**Lane B is on by default** (`recall:` in the configuration names the rule pack and the tools; `enabled: false` +turns it off). Its rules are MIT and Apache-2.0, and a repository scan whose rule pack or tools are missing exits 4, +never 0. A scan with a lane turned off and nothing found exits 3. + **The Trivy database is off by default** (the owner's decision of 2026-10-03). Trivy decides repository dependencies against its own database, which aggregates share-alike sources (Ubuntu, Alpine) that tier 2 quarantines, and whose publishers state no redistribution terms. Repository SCA therefore runs only when the diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md index b29b06a..44f67cd 100644 --- a/THIRD-PARTY-LICENSES.md +++ b/THIRD-PARTY-LICENSES.md @@ -100,18 +100,44 @@ attach when a release artifact bakes them in, which is the container image and t | Nuclei | MIT | Always-on DAST engine. Ships only in `anvil-dast`. | | ZAP | Apache-2.0 | Scheduled full scans. Ships only in `anvil-dast`. | | Syft / Grype / OSV-Scanner | Apache-2.0 | SBOM and matching. | -| opengrep (engine) | **LGPL-2.1** | The single strongest obligation in the set — see below. | -| `AikidoSec/opengrep-rules` | MIT | The rules corpus. **Not** `opengrep/opengrep-rules`, which is archived, `NOASSERTION`, and LGPL-2.1 + Commons Clause; it is on the spine's exclusion list. | +| opengrep (engine) 1.26.0 | **LGPL-2.1** | Lane B's rule engine. The single strongest obligation in the set — see below. | +| gosec 2.29.0 | Apache-2.0 | Lane B, Go. Read from `LICENSE.txt` in the pinned release archive. | +| bandit 1.9.4 | Apache-2.0 | Lane B, Python. | | llama.cpp / llama-server | MIT | Model serving. | | ONNX Runtime | MIT | Encoder path. | | honggfuzz | Apache-2.0 | The permissive AFL++ substitute. AFL++ is excluded: its LICENSE is AGPL-3.0 regardless of its README. | | llama-swap | MIT | Attribution in docs — `plan/design/spine-open-licences.md` (llama-swap). | + +## Rule corpora vendored in source form — `data/rules` + +Lane B's rules are committed, so their notices are owed now, not at packaging. `NOTICE` §1a states each +duty; `data/rules/MANIFEST.json` records, per rule file, its repository, commit, git blob id, SHA-256 and the +licence read from the stricter of its own header and its GitLab companion test file's (GitLab) or from its corpus's +LICENSE body (0xdea), and `internal/recall` refuses a +pack that does not match. + +| Corpus | Pin | Rule files | SPDX | Licence body archived | +|---|---|---|---|---| +| GitLab `sast-rules` | `53bf5cf6` | 58 (6 of them derived from JS Foundation code) | MIT | `data/rules/gitlab-sast-rules/LICENSE` | +| GitLab `sast-rules`, `go/` (derived from gosec) | `53bf5cf6` | 27 | Apache-2.0 | `data/rules/gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt` | +| GitLab `sast-rules`, `python/` (derived from bandit) | `53bf5cf6` | 52 | Apache-2.0 | `data/rules/gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt` | +| `0xdea/semgrep-rules`, `rules/c` | `dae50da6` | 39 | MIT | `data/rules/0xdea-semgrep-rules/LICENSE` | + +Excluded, with the reason in `data/rules/selection.json`: GitLab's `c/` (each rule headed "License: GPL 2.0", +generated from flawfinder); 131 Java, Scala and C# rules whose companions name find-sec-bugs or +security-code-scan (LGPL-3.0), each listed in `MANIFEST.json`; `doc/` (CC BY-SA 4.0 under GitLab's LICENSE), and everything outside the six +allowlisted directories; Semgrep's own rules; the archived `opengrep/opengrep-rules` (LGPL-2.1 + Commons Clause, +`NOASSERTION`); `lambdasec/autogrep` (GPL, AGPL and unlicensed rules inside an Apache-2.0 repository). +`AikidoSec/opengrep-rules` (MIT), the first plan's pick, is two GitHub-workflow rules and is not used. + **opengrep is LGPL-2.1 and it is invoked, never linked.** It is an OCaml CLI with zero bindings in any language (the spine's Go control-plane decision), so subprocess invocation is the only option that exists — which is also the option that keeps the LGPL boundary clean. Baking the binary into the container image is the -event that triggers offer-source obligations, and the container image must name where that offer is served rather than -leaving it implicit. +event that triggers source obligations (LGPL-2.1 §4), and the release binary is a PyInstaller bundle that also +carries GNU Readline, OpenSSL and Python packages under their own licences. `data/LICENSES/opengrep-binary-distribution.md` +sets out what it contains and the owner's decision of 2026-10-04: opengrep is an operator-installed +prerequisite, so no Anvil artifact includes it. ## sqlmap — a separate GPL-3.0 artifact, deliberately not a dependency diff --git a/cmd/anvil/main.go b/cmd/anvil/main.go index bb683c5..e3b57f8 100644 --- a/cmd/anvil/main.go +++ b/cmd/anvil/main.go @@ -17,8 +17,8 @@ // // A refusal and a missing tool are never 0. No flag widens what a scan may // touch: what a scan reads is the repository or inventory it is named, and -// whether Trivy-decided findings are admitted is the operator's configuration -// file, not a flag. +// whether Trivy-decided findings are admitted, and whether Lane B runs, is the +// operator's configuration file, not a flag. // // THIS BINARY HAS NO NETWORK-PROBING CAPABILITY. The dynamic tier is // cmd/anvil-dast, a separate artifact; TestSplit in split_test.go fails if @@ -47,6 +47,8 @@ import ( "github.com/Susquehanna-Syntax/Anvil/internal/ingest/delta" "github.com/Susquehanna-Syntax/Anvil/internal/ingest/offline" "github.com/Susquehanna-Syntax/Anvil/internal/ingest/poller" + "github.com/Susquehanna-Syntax/Anvil/internal/laneb" + "github.com/Susquehanna-Syntax/Anvil/internal/recall" "github.com/Susquehanna-Syntax/Anvil/internal/scan" "github.com/Susquehanna-Syntax/Anvil/internal/settings" "github.com/Susquehanna-Syntax/Anvil/internal/store" @@ -69,6 +71,7 @@ const usage = `usage: anvil scan --repo PATH [--target NAME] [--out FILE] [--event EVENT] [--full] [--policy FILE] anvil scan --host --inventory FILE|- [--out FILE] [--event EVENT] [--full] [--policy FILE] anvil findings [--target LOCATOR] [--all] + anvil recall PATH [FILE...] anvil feeds import DIR anvil dispatch (--repo PATH | --host --inventory FILE) --event EVENT [--full] anvil daemon [--once] @@ -77,6 +80,8 @@ const usage = `usage: Every command takes --config FILE (default $ANVIL_CONFIG, then /etc/anvil/anvil.yml). anvil scan --host does not collect: it reads the inventory the host collector (cmd/anvil-host-collector) writes when it runs under its systemd unit. +anvil recall prints Lane B's candidates for a tree as JSON and records nothing; +with FILEs, only candidates in those repository-relative files. ` func main() { @@ -99,6 +104,8 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { return cmdScan(ctx, args[1:], stdout, stderr) case "findings": return cmdFindings(ctx, args[1:], stdout, stderr) + case "recall": + return cmdRecall(ctx, args[1:], stdout, stderr) case "feeds": return cmdFeeds(ctx, args[1:], stdout, stderr) case "dispatch": @@ -209,7 +216,7 @@ func cmdScan(ctx context.Context, args []string, stdout, stderr io.Writer) int { Event: *event, Full: *full, PolicyPath: *policyPath, Cache: st.cache, Store: st.store, Feeds: st.feeds, TrivyDB: scan.TrivyDB{Enabled: st.cfg.TrivyDB}, Trivy: repo.DefaultConfig(), - AnvilVersion: version, + LaneB: laneBConfig(st.cfg), AnvilVersion: version, } if *hostScan { req.Kind = scan.KindHost @@ -243,6 +250,12 @@ func cmdScan(ctx context.Context, args []string, stdout, stderr io.Writer) int { for _, p := range res.Problems { fmt.Fprintf(stderr, "anvil scan: %s\n", p) } + for _, n := range res.NotRun { + fmt.Fprintf(stderr, "anvil scan: %s\n", n) + } + for _, n := range res.Notes { + fmt.Fprintf(stderr, "anvil scan: note: %s\n", n) + } switch res.Outcome { case scan.OutcomeFindings: return exitFindings @@ -254,6 +267,95 @@ func cmdScan(ctx context.Context, args []string, stdout, stderr io.Writer) int { return exitRefused } +// laneBConfig is Lane B's configuration from the operator's file, or nil when +// the file turns it off. +func laneBConfig(cfg settings.Settings) *laneb.Config { + if !cfg.Recall.Enabled { + return nil + } + r := cfg.Recall + return &laneb.Config{Rules: r.Rules, Tools: recall.Tools{ + Opengrep: r.Opengrep, Gosec: r.Gosec, Bandit: r.Bandit, GoBin: r.GoBin, + }} +} + +// cmdRecall prints Lane B's candidates for a tree: the recall tier exactly as +// a repository scan runs it, with nothing recorded. The candidates-per-scan +// instrument (eval/) measures through it, so the number it reports is the +// number a scan would record. +func cmdRecall(ctx context.Context, args []string, stdout, stderr io.Writer) int { + fs, configPath := newFlags("recall", stderr) + if err := fs.Parse(args); err != nil || fs.NArg() < 1 { + fmt.Fprintf(stderr, "anvil recall: give a PATH\n\n%s", usage) + return exitUsage + } + cfg, err := settings.Load(*configPath) + if err != nil { + fmt.Fprintf(stderr, "anvil recall: %v\n", err) + return exitError + } + lb := laneBConfig(cfg) + if lb == nil { + fmt.Fprint(stderr, "anvil recall: Lane B is off in the configuration (recall: {enabled: false})\n") + return exitRefused + } + pack, err := recall.LoadPack(lb.Rules) + if err != nil { + fmt.Fprintf(stderr, "anvil recall: %v\n", err) + return exitMissing + } + s := recall.Scanner{Pack: pack, Tools: lb.Tools} + plan, err := s.Prepare(ctx, fs.Arg(0)) + if err != nil { + fmt.Fprintf(stderr, "anvil recall: %v\n", err) + if errors.Is(err, recall.ErrToolAbsent) { + return exitMissing + } + return exitError + } + if only := fs.Args()[1:]; len(only) > 0 { + plan = plan.Only(only) + } + res, err := s.Scan(ctx, plan) + if err != nil { + fmt.Fprintf(stderr, "anvil recall: %v\n", err) + return exitError + } + if plan.Empty() { + // The same statement a repository scan makes: nothing was read, and + // that is not a clean tree. + res.Problems = append(res.Problems, "no file in a language Lane B's rules cover was given to any tool, so nothing was scanned") + } + out := struct { + Count int `json:"count"` + ByTool map[string]int `json:"byTool"` + Files int `json:"files"` + Excluded int `json:"excluded"` + ToolsRun []string `json:"toolsRun"` + Problems []string `json:"problems"` + PartialParses int `json:"partialParses"` + Candidates []recall.Candidate `json:"candidates"` + }{res.Count, res.ByTool, res.Files, plan.Excluded, res.ToolsRun, res.Problems, res.PartialParses, res.Candidates} + if out.Candidates == nil { + out.Candidates = []recall.Candidate{} + } + if out.Problems == nil { + out.Problems = []string{} + } + enc := json.NewEncoder(stdout) + enc.SetIndent("", " ") + if err := enc.Encode(out); err != nil { + return exitError + } + if len(res.Problems) > 0 { + return exitRefused + } + if res.Count > 0 { + return exitFindings + } + return exitClean +} + func writeRecord(path string, res scan.Result) error { b, err := json.MarshalIndent(res.Log, "", " ") if err != nil { @@ -389,7 +491,7 @@ func cmdDaemon(ctx context.Context, args []string, stderr io.Writer) int { Cache: st.cache, Store: st.store, Feeds: st.feeds, SpoolDir: st.cfg.SpoolDir, Policy: st.cfg.Policy, TrivyDB: scan.TrivyDB{Enabled: st.cfg.TrivyDB}, Trivy: repo.DefaultConfig(), - Version: version, Log: stderr, + LaneB: laneBConfig(st.cfg), Version: version, Log: stderr, } if len(st.feeds.Feeds) > 0 { p, err := poller.New(poller.Options{DB: st.cache, Mirror: os.DirFS(st.cfg.MirrorRoot)}) diff --git a/cmd/anvil/main_test.go b/cmd/anvil/main_test.go index 4b9a7ff..a51bdf5 100644 --- a/cmd/anvil/main_test.go +++ b/cmd/anvil/main_test.go @@ -40,6 +40,7 @@ func anvil(t *testing.T, args ...string) (int, string, string) { func TestExitStatuses(t *testing.T) { cfg := installation(t, "") + noLanes := installation(t, "recall:\n enabled: false\n") inv := filepath.Join(fixture, "host", "inventory-1.json") sarif := filepath.Join(t.TempDir(), "host.sarif") cases := []struct { @@ -54,7 +55,11 @@ func TestExitStatuses(t *testing.T) { {"scan with both targets", []string{"scan", "--config", cfg, "--repo", ".", "--host"}, exitUsage}, {"a host scan never collects itself", []string{"scan", "--config", cfg, "--host"}, exitRefused}, {"a host with findings", []string{"scan", "--config", cfg, "--host", "--inventory", inv, "--out", sarif}, exitFindings}, - {"repository SCA is off by default", []string{"scan", "--config", cfg, "--repo", filepath.Join(fixture, "repo")}, exitRefused}, + {"a repository scan with both lanes off is refused", []string{"scan", "--config", noLanes, "--repo", filepath.Join(fixture, "repo")}, exitRefused}, + {"Lane B is on by default, and its absent rule pack is a missing tool", []string{"scan", "--config", cfg, "--repo", filepath.Join(fixture, "repo")}, exitMissing}, + {"anvil recall with Lane B off is refused", []string{"recall", "--config", noLanes, "."}, exitRefused}, + {"anvil recall with no rule pack is a missing tool", []string{"recall", "--config", cfg, "."}, exitMissing}, + {"anvil recall with no path", []string{"recall", "--config", cfg}, exitUsage}, {"a scheduled scan with no policy is refused", []string{"scan", "--config", cfg, "--host", "--inventory", inv, "--full", "--event", "schedule"}, exitRefused}, {"findings in the store", []string{"findings", "--config", cfg}, exitFindings}, } diff --git a/data/LICENSES/opengrep-binary-distribution.md b/data/LICENSES/opengrep-binary-distribution.md new file mode 100644 index 0000000..fdc9b26 --- /dev/null +++ b/data/LICENSES/opengrep-binary-distribution.md @@ -0,0 +1,50 @@ +# opengrep: what shipping the binary would oblige + +opengrep, Lane B's rule engine, is LGPL-2.1 (its LICENSE body at tag v1.26.0, recorded in +`eval/tools/opengrep/LICENSES.md` §1). Anvil runs it as a separate process and links none of it, so a source +checkout of Anvil and an installation where the operator installed opengrep themselves owe nothing for it. + +## Distributing the binary is distributing the Library itself + +If an Anvil release artifact (the container image, a systemd tarball, any package) includes the opengrep +executable, Anvil distributes the LGPL-2.1 Library itself in object form. That is LGPL-2.1 **§4**, not §6 +(§6 is about a separate "work that uses the Library", which Anvil, as a separate process, is not): + +- carry the LGPL-2.1 text and opengrep's copyright notices with it (§1, by way of §4); +- accompany it with the complete corresponding machine-readable source for that exact version (opengrep + v1.26.0, commit `1bef4ea4ff3264754132eec823b5b1d8cde3e4ee`), or, if the binary is offered for download from a + designated place, offer equivalent access to the source from the same place (§4's last paragraph). + +## The binary is a bundle, and the bundle is not only opengrep + +The pinned release asset (`opengrep_manylinux_x86`, SHA-256 `40c21299…`) is a PyInstaller bundle. Unpacked on +2026-10-03 (`~/.cache/opengrep/v1.26.0/`, 65 entries), it carries, besides opengrep's own engine: + +- `libreadline.so.7` (GNU Readline 7) and `libtinfo.so.6`; +- OpenSSL 1.1 (`libssl.so.1.1`, `libcrypto.so.1.1`); +- CPython's runtime and extension modules, `libffi`, `libbz2`, `liblzma`, `libsqlite3`, `libmpdec`, `libuuid`; +- Python packages including `certifi`, `chardet` and `charset_normalizer`. + +The bundle contains no licence files for any of these. Their licences have **not** been read from their bodies +for this note: the same-family review of 2026-10-03 named GNU Readline 7 as GPL-3.0, certifi as MPL-2.0 and +chardet as LGPL-2.1, from memory, and those are the claims to verify first. If Readline 7 is GPL-3.0 as +expected, conveying the bundle conveys a GPL-3.0 library with GPL-3.0's source duties, which is a heavier +obligation than opengrep's own and must be settled before any artifact ships the binary. + +## The decision + +**The owner decided on 2026-10-04: opengrep is an operator-installed prerequisite, as Trivy is.** No Anvil +release artifact includes the opengrep binary, so Anvil conveys none of the components above and no LGPL or GPL +duty attaches to Anvil. Anvil checks the installed version against the pin on every scan +(`internal/recall`, `checkVersion`) and refuses a missing or unpinned one as a missing tool. The options weighed +were: + +| Option | What it costs | What it buys | +|---|---|---| +| Ship the release binary in the image | Every component above, with its own notices and source duties, verified licence by licence | Lane B works out of the box | +| Build opengrep from source for the image, without the bundled extras | A build pipeline and its own audit | Only what is built is conveyed | +| Fetch on first run from opengrep's release, verified by the SHA-256 in `eval/tools/opengrep/MANIFEST.toml` | A network fetch at install, an install step that can fail, and the fetch must happen before any scan, never at scan time | Anvil conveys nothing | +| An operator-installed prerequisite, like Trivy today | An extra install step for every operator | No duty, no fetch logic | + +Phase 9 (plan node packaging) owns the install documentation and must cite this file; changing the decision +(shipping the binary in an image, say) means reading every bundled component's licence from its body first. diff --git a/data/rules/0xdea-semgrep-rules/LICENSE b/data/rules/0xdea-semgrep-rules/LICENSE new file mode 100644 index 0000000..3eddae7 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 raptor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/data/rules/0xdea-semgrep-rules/rules/c/command-injection.yaml b/data/rules/0xdea-semgrep-rules/rules/c/command-injection.yaml new file mode 100644 index 0000000..92183f2 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/command-injection.yaml @@ -0,0 +1,40 @@ +rules: + - id: raptor-command-injection + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Command Injection + likelihood: MEDIUM + impact: HIGH + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')" + - "CWE-88: Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')" + - "CWE-676: Use of Potentially Dangerous Function" + owasp: + - A05:2025 - Injection + references: + - https://g.co/kgs/PCHQjJ + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + message: >- + The software constructs an OS command using externally-influenced input, + but it does not neutralize or incorrectly neutralizes special elements + that could modify the intended OS command. + severity: ERROR + languages: + - c + - cpp + # NOTE: add path injection rule for `execlp`, `execvp`, and `execvP`. + # NOTE: add argument injection (e.g., in the `exec` functions family). + patterns: + - pattern-either: + - pattern: system(...) + - pattern: popen(...) + - pattern: p2open(...) + - pattern: wordexp(...) + - pattern-not: $_("...", ...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/double-free.yaml b/data/rules/0xdea-semgrep-rules/rules/c/double-free.yaml new file mode 100644 index 0000000..6cefafd --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/double-free.yaml @@ -0,0 +1,55 @@ +rules: + - id: raptor-double-free + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-415: Double Free" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://github.com/struct/mms + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + - https://docs.microsoft.com/en-us/cpp/sanitizers/asan-error-examples + - https://dustri.org/b/playing-with-weggli.html + message: >- + The software calls free() twice on the same memory address, potentially + leading to memory corruption. This corruption can cause the program to + crash or cause two later calls to malloc() to return the same pointer. + severity: ERROR + languages: + - c + - cpp + # NOTE: C++ `delete` and `delete[]` operators and custom wrappers to `free` are not covered. + # NOTE: `realloc` is not covered. + # NOTE: variations on the `free` argument are not covered. + patterns: + - pattern: | + $FREE1($PTR); + ... + $FREE2($PTR); + - pattern-not: | + $FREE1($PTR); + ... + $PTR = ...; + ... + $FREE2($PTR); + - metavariable-pattern: + metavariable: $FREE1 + pattern: free + # more patterns could be added here to cover other ways to free memory + - metavariable-pattern: + metavariable: $FREE2 + pattern: free + # more patterns could be added here to cover other ways to free memory + # improve output readability + - focus-metavariable: $FREE2 diff --git a/data/rules/0xdea-semgrep-rules/rules/c/format-string-bugs.yaml b/data/rules/0xdea-semgrep-rules/rules/c/format-string-bugs.yaml new file mode 100644 index 0000000..350270e --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/format-string-bugs.yaml @@ -0,0 +1,122 @@ +rules: + - id: raptor-format-string-bugs + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-134: Use of Externally-Controlled Format String" + owasp: + - A05:2025 - Injection + references: + - https://julianor.tripod.com/bc/formatstring-1.2.pdf + - http://phrack.org/issues/70/13.html#article + - https://g.co/kgs/PCHQjJ + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + message: >- + The software uses a function that accepts a format string as an argument, + but the format string originates from an external source. This can lead + to buffer overflows, denial of service, or data representation problems. + severity: ERROR + languages: + - c + - cpp + # NOTE: generic `va_list` matching for custom wrappers is not covered. + # NOTE: `dprintf` in C++ has a different signature; this rule may cause false positives with it. + # NOTE: `#define` format specifiers are not excluded due to Semgrep limitations; perhaps we could implement this with symbolic propagation (Semgrep Pro experimental feature)? + # NOTE: some obsolete Microsoft-specific functions, such as `_ftprintf`, `_sntprintf`, `_snwprintf`, `_sntscanf`, etc., are not covered. + pattern-either: + # format string in 1st arg + - patterns: + - pattern-either: + # printf family + - pattern: printf(...) + - pattern: vprintf(...) + - pattern: wprintf(...) + - pattern: vwprintf(...) + - pattern: vcprintf(...) + - pattern: vcwprintf(...) + - pattern: vscprintf(...) + - pattern: vscwprintf(...) + - pattern: printk(...) + # scanf family + - pattern: scanf(...) + - pattern: __isoc99_scanf(...) + - pattern: vscanf(...) + - pattern: __isoc99_vscanf(...) + - pattern: wscanf(...) + - pattern: vwscanf(...) + # err/warn family + - pattern: warn(...) + - pattern: vwarn(...) + - pattern: warnx(...) + - pattern: vwarnx(...) + - pattern-not: $_("...", ...) + - pattern-not: $_(gettext(...), ...) + - pattern-not: $_(dgettext(...), ...) + - pattern-not: $_(dcgettext(...), ...) + # format string in 2nd arg + - patterns: + - pattern-either: + # printf family + - pattern: fprintf(...) + - pattern: vfprintf(...) + - pattern: fwprintf(...) + - pattern: vfwprintf(...) + - pattern: sprintf(...) + - pattern: vsprintf(...) + - pattern: asprintf(...) + - pattern: vasprintf(...) + - pattern: dprintf(...) + - pattern: vdprintf(...) + - pattern: wsprintf(...) + # scanf family + - pattern: fscanf(...) + - pattern: __isoc99_fscanf(...) + - pattern: vfscanf(...) + - pattern: __isoc99_vfscanf(...) + - pattern: fwscanf(...) + - pattern: vfwscanf(...) + - pattern: sscanf(...) + - pattern: __isoc99_sscanf(...) + - pattern: vsscanf(...) + - pattern: __isoc99_vsscanf(...) + - pattern: swscanf(...) + - pattern: vswscanf(...) + # syslog family + - pattern: syslog(...) + - pattern: vsyslog(...) + # err/warn family + - pattern: err(...) + - pattern: verr(...) + - pattern: errx(...) + - pattern: verrx(...) + - pattern: warnc(...) + - pattern: vwarnc(...) + - pattern-not: $_($_, "...", ...) + - pattern-not: $_($_, gettext(...), ...) + - pattern-not: $_($_, dgettext(...), ...) + - pattern-not: $_($_, dcgettext(...), ...) + # format string in 3rd arg + - patterns: + - pattern-either: + # printf family + - pattern: snprintf(...) + - pattern: vsnprintf(...) + - pattern: swprintf(...) + - pattern: vswprintf(...) + # err/warn family + - pattern: errc(...) + - pattern: verrc(...) + - pattern-not: $_($_, $_, "...", ...) + - pattern-not: $_($_, $_, gettext(...), ...) + - pattern-not: $_($_, $_, dgettext(...), ...) + - pattern-not: $_($_, $_, dcgettext(...), ...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/incorrect-order-setuid-setgid.yaml b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-order-setuid-setgid.yaml new file mode 100644 index 0000000..28a1f8d --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-order-setuid-setgid.yaml @@ -0,0 +1,94 @@ +rules: + - id: raptor-incorrect-order-setuid-setgid + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Improper Authorization + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-696: Incorrect Behavior Order" + owasp: + - A01:2025 - Broken Access Control + references: + - https://www.usenix.org/legacy/events/sec02/full_papers/chen/chen.pdf + - https://g.co/kgs/PCHQjJ + - https://github.com/struct/mms + message: >- + A compromised process might be able to regain elevated group privileges + if set(e)gid() is called after set(e)uid(). A similar case is when + privileges are temporarily dropped with seteuid() and then setuid() or + seteuid() are called from while under unprivileged user. + severity: ERROR + languages: + - c + - cpp + pattern-either: + # setuid/setgid + - patterns: + - pattern: | + <... setuid(...) ...>; + ... + <... setgid(...) ...>; + - pattern-not: | + <... setuid(0) ...>; + ... + <... setgid(...) ...>; + # seteuid/setegid + - patterns: + - pattern: | + <... seteuid(...) ...>; + ... + <... setegid(...) ...>; + - pattern-not: | + <... seteuid(0) ...>; + ... + <... setegid(...) ...>; + - pattern-not-inside: | + <... seteuid(...) ...>; + ... + <... seteuid(0) ...>; + ... + <... setegid(...) ...>; + # seteuid/setuid + - patterns: + - pattern: | + <... seteuid(...) ...>; + ... + <... setuid(...) ...>; + - pattern-not: | + <... seteuid(0) ...>; + ... + <... setuid(...) ...>; + - pattern-not-inside: | + <... seteuid(...) ...>; + ... + <... seteuid(0) ...>; + ... + <... setuid(...) ...>; + # seteuid/seteuid + - patterns: + - pattern: | + <... seteuid(...) ...>; + ... + <... seteuid(...) ...>; + - pattern-not: | + <... seteuid(0) ...>; + ... + <... seteuid(...) ...>; + - pattern-not: | + <... seteuid(...) ...>; + ... + <... seteuid(0) ...>; + - pattern-not-inside: | + <... seteuid(...) ...>; + ... + <... seteuid(0) ...>; + ... + <... seteuid(...) ...>; diff --git a/data/rules/0xdea-semgrep-rules/rules/c/incorrect-unsigned-comparison.yaml b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-unsigned-comparison.yaml new file mode 100644 index 0000000..549c7e8 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-unsigned-comparison.yaml @@ -0,0 +1,52 @@ +rules: + - id: raptor-incorrect-unsigned-comparison + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Improper Validation + likelihood: LOW + impact: MEDIUM + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-697: Incorrect Comparison" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + message: >- + Checking if an unsigned variable is negative makes no sense and is + usually a good indication that something is probably wrong with the + code. + severity: WARNING + languages: + - c + - cpp + # NOTE: some types are not covered. + # NOTE: incorrect unsigned assigments are not covered. + pattern-either: + # < (always false) + - pattern: (unsigned short $_) < 0 + - pattern: (unsigned short int $_) < 0 + - pattern: (unsigned int $_) < 0 + - pattern: (unsigned long $_) < 0 + - pattern: (unsigned long int $_) < 0 + - pattern: (size_t $_) < 0 + # <= + - pattern: (unsigned short $_) <= 0 + - pattern: (unsigned short int $_) <= 0 + - pattern: (unsigned int $_) <= 0 + - pattern: (unsigned long $_) <= 0 + - pattern: (unsigned long int $_) <= 0 + - pattern: (size_t $_) <= 0 + # >= (always true) + - pattern: (unsigned short $_) >= 0 + - pattern: (unsigned short int $_) >= 0 + - pattern: (unsigned int $_) >= 0 + - pattern: (unsigned long $_) >= 0 + - pattern: (unsigned long int $_) >= 0 + - pattern: (size_t $_) >= 0 diff --git a/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-free.yaml b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-free.yaml new file mode 100644 index 0000000..a87f0a4 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-free.yaml @@ -0,0 +1,86 @@ +rules: + - id: raptor-incorrect-use-of-free + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-590: Free of Memory not on the Heap" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://github.com/shellphish/how2heap/blob/master/glibc_2.23/house_of_spirit.c + message: >- + The software calls free() on a pointer to memory that has a short + lifetime and was not allocated using associated heap allocation + functions such as malloc(), calloc(), or realloc(). + severity: ERROR + languages: + - c + - cpp + # NOTE: this rule only covers `free` called on stack-allocated buffers (and possibly static variables); it does not cover other instances of `free` called on non-heap-allocated buffers; it also does not cover `alloca` and custom wrappers to `free`. + # NOTE: multi-dimensional arrays and other ways to create an array are not covered. + # NOTE: refer also to `raptor-mismatched-memory-management.yaml`. + pattern-either: + - patterns: + - pattern: free($PTR) + - pattern-either: + # array + - pattern-inside: | + $_ $PTR[$_]; + ... + - pattern-inside: | + $_ $PTR[$_] = ...; + ... + - pattern-inside: | + $_ $PTR[] = ...; + ... + # pointer to array + - pattern-inside: | + $_ $ARR[$_]; + ... + $PTR = $ARR; + ... + - pattern-inside: | + $_ $ARR[$_] = ...; + ... + $PTR = $ARR; + ... + - pattern-inside: | + $_ $ARR[] = ...; + ... + $PTR = $ARR; + ... + - patterns: + # address of local variable + - pattern: free(&$VAR) + - pattern-either: + - pattern-inside: | + $_ $VAR; + ... + - pattern-inside: | + $_ $VAR = ...; + ... + - pattern-inside: | + $_ $VAR[$_]; + ... + - pattern-inside: | + $_ $VAR[$_] = ...; + ... + - pattern-inside: | + $_ $VAR[] = ...; + ... + - pattern-inside: | + $_ * $VAR; + ... + - pattern-inside: | + $_ * $VAR = ...; + ... diff --git a/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-memset.yaml b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-memset.yaml new file mode 100644 index 0000000..5467115 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-memset.yaml @@ -0,0 +1,37 @@ +rules: + - id: raptor-incorrect-use-of-memset + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Mishandled Sensitive Information + likelihood: MEDIUM + impact: MEDIUM + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-683: Function Call With Incorrect Order of Arguments" + owasp: + - A04:2025 - Cryptographic Failures + references: + - https://lkml.org/lkml/2009/11/11/336 + - https://bugs.mysql.com/bug.php?id=23181 + message: >- + The invocation of memset() is easy to get wrong. The second argument + is the character and the third argument is the size, but sometimes + these arguments are in the wrong order. This results in a no-op. + severity: ERROR + languages: + - c + - cpp + # NOTE: refer also to `c.lang.security.insecure-use-memset.insecure-use-memset` and `cpp.lang.correctness.memset-removal.memset-removal` for a different potential flaw involving `memset` optimization. + pattern-either: + - pattern: memset($_, $_, 0) + - pattern: memset($_, $_, '\0') + - pattern: memset($_, sizeof(...), $_) + - pattern: memset_explicit($_, $_, 0) + - pattern: memset_explicit($_, $_, '\0') + - pattern: memset_explicit($_, sizeof(...), $_) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-sizeof.yaml b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-sizeof.yaml new file mode 100644 index 0000000..1e7b26f --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-sizeof.yaml @@ -0,0 +1,78 @@ +rules: + - id: raptor-incorrect-use-of-sizeof + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-467: Use of sizeof() on a Pointer Type" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + - https://github.com/struct/mms + - https://dustri.org/b/playing-with-weggli.html + - https://mastodon.gamedev.place/@amonakov/111577826195014479 + - https://github.com/semgrep/semgrep/issues/11327 + message: >- + The code calls sizeof() on a pointer type, which always returns + the wordsize (8 on 64-bit architectures). This can produce an + unexpected result if the programmer intended to determine how + much memory has been allocated. + severity: WARNING + languages: + - c + - cpp + # NOTE: another relevant pattern would be `sizeof('...')`, but unfortunately that's currently not supported by Semgrep. + pattern-either: + # type-based patterns (some types are missing) + - patterns: + - pattern: sizeof((char * $_)) + - pattern-not: sizeof("...") + # NOTE: sizeof of character literals is still not supported by Semgrep as of 2025-11-22 + # - pattern: sizeof('...') + - pattern: sizeof((int * $_)) + - pattern: sizeof((float * $_)) + - pattern: sizeof((double * $_)) + # other patterns + - patterns: + - pattern-either: + - pattern: sizeof($PTR) + - pattern: sizeof($PTR + $_) + - pattern: sizeof($_ + $PTR) + - pattern-either: + - pattern-inside: | + $_ * $PTR; + ... + - pattern-inside: | + $_ * $PTR = ...; + ... + # function calls + - patterns: + - pattern-either: + - pattern: sizeof($PTR) + - pattern: sizeof($PTR + $_) + - pattern: sizeof($_ + $PTR) + - pattern-either: + - pattern-inside: | + $_ $_(..., $_ * $PTR, ...) { + ... + } + - pattern-inside: | + $_ $_(..., $_ $PTR[$_], ...) { + ... + } + - pattern-inside: | + $_ $_(..., $_ $PTR[], ...) { + ... + } + # c++ `this` pattern + - pattern: sizeof(this) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-strncat.yaml b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-strncat.yaml new file mode 100644 index 0000000..478ddf0 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/incorrect-use-of-strncat.yaml @@ -0,0 +1,50 @@ +rules: + - id: raptor-incorrect-use-of-strncat + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-787: Out-of-bounds Write" + - "CWE-193: Off-by-one Error" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + message: >- + The strncat() function is nearly as dangerous as strcat(), in that it's + quite easy to misuse. The first common mistake is supplying the size of + the entire buffer instead of the size remaining in the buffer. A more + subtle mistake can be made: the size parameter needs to be the amount of + space left in the buffer less one; otherwise, the NUL byte is written + one byte past the end of the buffer. + severity: ERROR + languages: + - c + - cpp + pattern-either: + # size of array + - patterns: + - pattern: strncat($DST, $_, $LEN) + - pattern-either: + - pattern-inside: | + $_ $DST[$LEN]; + ... + - pattern-inside: | + $_ $DST[$LEN] = ...; + ... + # sizeof operator + - pattern: strncat($DST, $_, sizeof($DST)) + # strlen function + - pattern: strncat($DST, $_, strlen($DST)) + # off-by-one + # see also off-by-one.yaml for a slightly different pattern + - pattern: strncat($DST, $_, sizeof($DST) - strlen($DST)) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-access-stat.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-access-stat.yaml new file mode 100644 index 0000000..092204a --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-access-stat.yaml @@ -0,0 +1,39 @@ +rules: + - id: raptor-insecure-api-access-stat + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Dangerous Method or Function + likelihood: MEDIUM + impact: MEDIUM + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + - https://labs.snyk.io/resources/leaky-vessels-container-vuln-deep-dive/ + message: >- + The software checks the state of a resource before using it, but the + resource's state can change between the check and the use in a way that + invalidates the results of the check. This can cause the software to + perform invalid actions when the resource is in an unexpected state. + severity: WARNING + languages: + - c + - cpp + # NOTE: `_stat` and `_lstat` are not covered. + # NOTE: refer also to `cpp.lang.security.filesystem.*` for stricter check/action patterns. + pattern-either: + - pattern: access(...) + - pattern: faccessat(...) + - pattern: faccessat2(...) + - pattern: stat(...) + - pattern: lstat(...) + - pattern: fstatat(...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-alloca.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-alloca.yaml new file mode 100644 index 0000000..c994acb --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-alloca.yaml @@ -0,0 +1,32 @@ +rules: + - id: raptor-insecure-api-alloca + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Dangerous Method or Function + likelihood: MEDIUM + impact: HIGH + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-676: Use of Potentially Dangerous Function" + - "CWE-1325: Improperly Controlled Sequential Memory Allocation" + owasp: + - A06:2025 - Insecure Design + references: + - https://nullprogram.com/blog/2019/10/28/ + - https://docs.microsoft.com/en-us/cpp/sanitizers/asan-error-examples + message: >- + Use of alloca() is unsafe because it cannot ensure that the pointer + returned points to a valid and usable block of memory. The allocation + made may exceed the bounds of the stack, or even go further into other + objects in memory, and alloca() cannot determine such an error. + severity: ERROR + languages: + - c + - cpp + pattern: alloca(...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-ato.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-ato.yaml new file mode 100644 index 0000000..a7f9e64 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-ato.yaml @@ -0,0 +1,39 @@ +rules: + - id: raptor-insecure-api-ato + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Dangerous Method or Function + likelihood: LOW + impact: MEDIUM + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-839: Numeric Range Comparison Without Minimum Check" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://rules.sonarsource.com/c/type/Bug/RSPEC-989 + - https://wiki.sei.cmu.edu/confluence/display/c/ERR34-C.+Detect+errors+when+converting+a+string+to+a+number + - https://github.com/vixie/cron/commit/9cc8ab1087bb9ab861dd5595c41200683c9f6712 + message: >- + The atoi(), atol(), atof(), and similar functions don't handle errors. + They don't check for integer overflow and can return a negative value. + They have undefined behavior if the value of the result cannot be + represented. They return 0 (or 0.0) if the string does not represent + an integer (or decimal), which is indistinguishable from a correctly + formatted, zero-denoting input string. + severity: INFO + languages: + - c + - cpp + # NOTE: some function variants are missing. + pattern-either: + - pattern: atoi(...) + - pattern: atol(...) + - pattern: atof(...) + - pattern: atoll(...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-gets.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-gets.yaml new file mode 100644 index 0000000..4f67b55 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-gets.yaml @@ -0,0 +1,30 @@ +rules: + - id: raptor-insecure-api-gets + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Dangerous Method or Function + likelihood: HIGH + impact: HIGH + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-242: Use of Inherently Dangerous Function" + - "CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + message: >- + The gets() function is always unsafe because it does not perform bounds + checking on the size of its input. An attacker can easily send + arbitrarily-sized input to gets() and overflow the destination buffer. + severity: ERROR + languages: + - c + - cpp + pattern: gets(...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-mktemp-tmpnam-tempnam.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-mktemp-tmpnam-tempnam.yaml new file mode 100644 index 0000000..682f26f --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-mktemp-tmpnam-tempnam.yaml @@ -0,0 +1,37 @@ +rules: + - id: raptor-insecure-api-mktemp-tmpnam-tempnam + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Dangerous Method or Function + likelihood: MEDIUM + impact: MEDIUM + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-377: Insecure Temporary File" + - "CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + message: >- + Creating and using insecure temporary files can leave application and + system data vulnerable to attack. + severity: WARNING + languages: + - c + - cpp + pattern-either: + - pattern: mktemp(...) + - pattern: tmpnam(...) + - pattern: tempnam(...) + - pattern: tmpnam_r(...) + # - pattern: _mktemp(...) + # - pattern: _tmpnam(...) + # - pattern: _tempnam(...) + # - pattern: GetTempFileName(...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-rand-srand.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-rand-srand.yaml new file mode 100644 index 0000000..42c3b9a --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-rand-srand.yaml @@ -0,0 +1,44 @@ +rules: + - id: raptor-insecure-api-rand-srand + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Cryptographic Issues + likelihood: MEDIUM + impact: MEDIUM + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)" + - "CWE-330: Use of Insufficiently Random Values" + owasp: + - A04:2025 - Cryptographic Failures + references: + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + message: >- + The rand() PRNG should not be used in applications intended to be + portable when good randomness is needed. Instead, the use of random() + is recommended. + severity: WARNING + languages: + - c + - cpp + # NOTE: `rand_r` and other non-standard PRNGs are not covered. + # NOTE: improperly seeded strong PRNGs are not covered. + # NOTE: refer also to `cpp.lang.security.rng.*`, `cpp.lang.security.crypto.*`, and `gitlab.flawfinder.*`. + pattern-either: + - pattern: rand(...) + - pattern: srand(...) + - pattern: drand48(...) + - pattern: erand48(...) + - pattern: lrand48(...) + - pattern: nrand48(...) + - pattern: mrand48(...) + - pattern: jrand48(...) + - pattern: lcong48(...) + - pattern: srand48(...) + - pattern: seed48(...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-scanf.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-scanf.yaml new file mode 100644 index 0000000..622777a --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-scanf.yaml @@ -0,0 +1,61 @@ +rules: + - id: raptor-insecure-api-scanf + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Dangerous Method or Function + likelihood: HIGH + impact: HIGH + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-676: Use of Potentially Dangerous Function" + - "CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')" + - "CWE-787: Out-of-bounds Write" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + message: >- + A buffer overflow condition exists when a program attempts to put + more data in a buffer than it can hold, or when a program attempts to + put data in a memory area outside of the boundaries of a buffer. + severity: ERROR + languages: + - c + - cpp + patterns: + - pattern-either: + - pattern: scanf($FMT, ...) + - pattern: __isoc99_scanf($FMT, ...) + - pattern: vscanf($FMT, ...) + - pattern: __isoc99_vscanf($FMT, ...) + - pattern: fscanf($_, $FMT, ...) + - pattern: __isoc99_fscanf($_, $FMT, ...) + - pattern: vfscanf($_, $FMT, ...) + - pattern: __isoc99_vfscanf($_, $FMT, ...) + - pattern: sscanf($_, $FMT, ...) + - pattern: __isoc99_sscanf($_, $FMT, ...) + - pattern: vsscanf($_, $FMT, ...) + - pattern: __isoc99_vsscanf($_, $FMT, ...) + - pattern: wscanf($FMT, ...) + - pattern: vwscanf($FMT, ...) + - pattern: fwscanf($_, $FMT, ...) + - pattern: vfwscanf($_, $FMT, ...) + - pattern: swscanf($_, $FMT, ...) + - pattern: vswscanf($_, $FMT, ...) + # exclude string literals as source + - pattern-not: sscanf("...", $FMT, ...) + - pattern-not: __isoc99_sscanf("...", $FMT, ...) + - pattern-not: vsscanf("...", $FMT, ...) + - pattern-not: __isoc99_vsscanf("...", $FMT, ...) + - pattern-not: swscanf("...", $FMT, ...) + - pattern-not: vswscanf("...", $FMT, ...) + - metavariable-regex: + metavariable: $FMT + # NOTE: some format string specifiers are not handled + regex: '(".*%l?s.*"|".*%S.*"|[a-zA-Z_][a-zA-Z0-9_]*)' diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-signal.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-signal.yaml new file mode 100644 index 0000000..7029f95 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-signal.yaml @@ -0,0 +1,35 @@ +rules: + - id: raptor-insecure-api-signal + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Dangerous Method or Function + likelihood: MEDIUM + impact: MEDIUM + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-364: Signal Handler Race Condition" + - "CWE-479: Signal Handler Use of a Non-reentrant Function" + - "CWE-828: Signal Handler with Functionality that is not Asynchronous-Safe" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://lcamtuf.coredump.cx/signals.txt + message: >- + The signal() API should be regarded as deprecated. When possible, + sigaction() should be used instead, because it allows to precisely + specify the desired behavior in case two signals arrive shortly after + each other thus preventing many race conditions. + severity: WARNING + languages: + - c + - cpp + pattern-either: + - pattern: signal(...) + - pattern: gsignal(...) + - pattern: ssignal(...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-sprintf-vsprintf.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-sprintf-vsprintf.yaml new file mode 100644 index 0000000..a250134 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-sprintf-vsprintf.yaml @@ -0,0 +1,38 @@ +rules: + - id: raptor-insecure-api-sprintf-vsprintf + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Dangerous Method or Function + likelihood: HIGH + impact: HIGH + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-676: Use of Potentially Dangerous Function" + - "CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')" + - "CWE-787: Out-of-bounds Write" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + message: >- + A buffer overflow condition exists when a program attempts to put + more data in a buffer than it can hold, or when a program attempts to + put data in a memory area outside of the boundaries of a buffer. + severity: ERROR + languages: + - c + - cpp + patterns: + - pattern-either: + - pattern: sprintf($_, $FMT, ...) + - pattern: vsprintf($_, $FMT, ...) + - metavariable-regex: + metavariable: $FMT + # NOTE: some format string specifiers are not handled + regex: '(".*%l?s.*"|".*%S.*"|[a-zA-Z_][a-zA-Z0-9_]*)' diff --git a/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-strcpy-strcat.yaml b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-strcpy-strcat.yaml new file mode 100644 index 0000000..bbe2119 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/insecure-api-strcpy-strcat.yaml @@ -0,0 +1,47 @@ +rules: + - id: raptor-insecure-api-strcpy-strcat + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Dangerous Method or Function + likelihood: HIGH + impact: HIGH + confidence: HIGH + technology: + - c + - cpp + cwe: + - "CWE-676: Use of Potentially Dangerous Function" + - "CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')" + - "CWE-787: Out-of-bounds Write" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + message: >- + A buffer overflow condition exists when a program attempts to put + more data in a buffer than it can hold, or when a program attempts to + put data in a memory area outside of the boundaries of a buffer. + severity: ERROR + languages: + - c + - cpp + # NOTE: some functions, such as `_fstrcpy` and `_fstrcat`, are not covered. + patterns: + - pattern-either: + - pattern: strcpy(...) + - pattern: stpcpy(...) + - pattern: strcat(...) + - pattern: wcscpy(...) + - pattern: wcpcpy(...) + - pattern: wcscat(...) + - pattern: _mbscpy(...) + - pattern: _mbscat(...) + - pattern: strccpy(...) + - pattern: strcadd(...) + - pattern: streadd(...) + - pattern: strecpy(...) + - pattern: strtrns(...) + - pattern-not: $_($_, "...", ...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/integer-truncation.yaml b/data/rules/0xdea-semgrep-rules/rules/c/integer-truncation.yaml new file mode 100644 index 0000000..4987332 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/integer-truncation.yaml @@ -0,0 +1,133 @@ +rules: + - id: raptor-integer-truncation + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Other Security + likelihood: MEDIUM + impact: MEDIUM + confidence: LOW + technology: + - c + - cpp + cwe: + - "CWE-197: Numeric Truncation Error" + - "CWE-681: Incorrect Conversion between Numeric Types" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + - https://github.com/struct/mms + message: >- + Truncation errors occur when a primitive is cast to a primitive of a + smaller size and data is lost in the conversion. The value cannot be + trusted and the application will be in an undefined state. + severity: WARNING + languages: + - c + - cpp + # NOTE: the deep expression operator is necessary to catch some matches because C/C++ integer promotions do not seem entirely supported by Semgrep; this may generate lots of false positives. + # NOTE: some types (e.g., pointers) and use cases (e.g., comparisons) are not covered. + pattern-either: + # char: assignments + - patterns: + - pattern-either: + - pattern: (char $_) = <... (short $_) ...> + - pattern: (char $_) = <... (short int $_) ...> + - pattern: (char $_) = <... (unsigned short $_) ...> + - pattern: (char $_) = <... (unsigned short int $_) ...> + - pattern: (char $_) = <... (int $_) ...> + - pattern: (char $_) = <... (unsigned $_) ...> + - pattern: (char $_) = <... (unsigned int $_) ...> + - pattern: (char $_) = <... (long $_) ...> + - pattern: (char $_) = <... (long int $_) ...> + - pattern: (char $_) = <... (unsigned long $_) ...> + - pattern: (char $_) = <... (unsigned long int $_) ...> + - pattern-not: $_ = 0 + # char: function return + - patterns: + - pattern-either: + - pattern: | + char $FUN(...) + { + ... + return (short $_); + } + - pattern: | + char $FUN(...) + { + ... + return (int $_); + } + - pattern: | + char $FUN(...) + { + ... + return (long $_); + } + # improve output readability + - focus-metavariable: $FUN + # short: assignments + - patterns: + - pattern-either: + - pattern: (short $_) = <... (unsigned short $_) ...> + - pattern: (short int $_) = <... (unsigned short int $_) ...> + - pattern: (short $_) = <... (int $_) ...> + - pattern: (short $_) = <... (unsigned $_) ...> + - pattern: (short int $_) = <... (unsigned int $_) ...> + - pattern: (short $_) = <... (long $_) ...> + - pattern: (short int $_) = <... (long int $_) ...> + - pattern: (short $_) = <... (unsigned long $_) ...> + - pattern: (short int $_) = <... (unsigned long int $_) ...> + - pattern: (unsigned short $_) = <... (int $_) ...> + - pattern: (unsigned short int $_) = <... (unsigned int $_) ...> + - pattern: (unsigned short $_) = <... (long $_) ...> + - pattern: (unsigned short int $_) = <... (long int $_) ...> + - pattern: (unsigned short $_) = <... (unsigned long $_) ...> + - pattern: (unsigned short int $_) = <... (unsigned long int $_) ...> + - pattern-not: $_ = 0 + # short: function return + - patterns: + - pattern-either: + - pattern: | + short $FUN(...) + { + ... + return (int $_); + } + - pattern: | + short $FUN(...) + { + ... + return (long $_); + } + # improve output readability + - focus-metavariable: $FUN + # int: assignments + - patterns: + - pattern-either: + - pattern: (int $_) = <... (unsigned $_) ...> + - pattern: (int $_) = <... (unsigned int $_) ...> + - pattern: (int $_) = <... (long $_) ...> + - pattern: (int $_) = <... (long int $_) ...> + - pattern: (int $_) = <... (unsigned long $_) ...> + - pattern: (int $_) = <... (unsigned long int $_) ...> + - pattern-not: $_ = 0 + # int: function return + - patterns: + - pattern: | + int $FUN(...) + { + ... + return (long $_); + } + # improve output readability + - focus-metavariable: $FUN + # long: assignments + - patterns: + - pattern-either: + - pattern: (long $_) = <... (unsigned long $_) ...> + - pattern: (long int $_) = <... (unsigned long int $_) ...> + - pattern-not: $_ = 0 diff --git a/data/rules/0xdea-semgrep-rules/rules/c/integer-wraparound.yaml b/data/rules/0xdea-semgrep-rules/rules/c/integer-wraparound.yaml new file mode 100644 index 0000000..c2f477b --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/integer-wraparound.yaml @@ -0,0 +1,214 @@ +rules: + - id: raptor-integer-wraparound + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-190: Integer Overflow or Wraparound" + - "CWE-128: Wrap-around Error" + - "CWE-131: Incorrect Calculation of Buffer Size" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + - http://www.phrack.org/issues/60/10.html#article + - https://docs.microsoft.com/en-us/cpp/sanitizers/asan-error-examples + - https://dustri.org/b/playing-with-weggli.html + message: >- + The software performs a calculation that can produce an integer + overflow or wraparound, when the logic assumes that the resulting + value will always be larger than the original value. This can + introduce other weaknesses when the calculation is used for resource + management or execution control. + severity: WARNING + languages: + - c + - cpp + # NOTE: only multiplication and sum with two operands are covered; other arithmetic operations should also be considered (e.g., left shift, even subtraction), as well as any operation with more than two operands. + # NOTE: some allocation functions such as `kmalloc`, `HeapAlloc`, and custom wrappers are not covered. + # NOTE: there might exist other buggy input validation patterns not covered here, including the same patterns with a different order of operands. + # NOTE: consider excluding `strlen` patterns to avoid false positives (at the cost of some false negatives). + # NOTE: we should do some tests with the deep expression operator, refer also to `cpp.lang.security.memory.integer-overflow-allocation.integer-overflow-allocation`. + # NOTE: a dedicated rule for C++ operators such as `new` should be implemented. + pattern-either: + # malloc, valloc (multiplication) + - patterns: + - pattern-either: + - pattern: $FUN($_ * $_) + - pattern: $FUN($_ *= $_) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: malloc + - pattern: valloc + - patterns: + - pattern: $FUN($N) + - pattern-either: + - pattern-inside: | + $N = $_ * $_; + ... + - pattern-inside: | + $N *= $_; + ... + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: malloc + - pattern: valloc + # malloc, valloc (sum) + - patterns: + - pattern-either: + - pattern: $FUN($_ + $_) + - pattern: $FUN($_ += $_) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: malloc + - pattern: valloc + - patterns: + - pattern: $FUN($N) + - pattern-either: + - pattern-inside: | + $N = $_ + $_; + ... + - pattern-inside: | + $N += $_; + ... + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: malloc + - pattern: valloc + # calloc, realloc, reallocf, aligned_alloc, VirtualAlloc (multiplication) + - patterns: + - pattern-either: + - pattern: $FUN($_, $_ * $_, ...) + - pattern: $FUN($_, $_ *= $_, ...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: calloc + - pattern: realloc + - pattern: reallocf + - pattern: aligned_alloc + - pattern: VirtualAlloc + - patterns: + - pattern: $FUN($_, $N, ...) + - pattern-either: + - pattern-inside: | + $N = $_ * $_; + ... + - pattern-inside: | + $N *= $_; + ... + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: calloc + - pattern: realloc + - pattern: reallocf + - pattern: aligned_alloc + - pattern: VirtualAlloc + # calloc, realloc, reallocf, aligned_alloc, VirtualAlloc (sum) + - patterns: + - pattern-either: + - pattern: $FUN($_, $_ + $_, ...) + - pattern: $FUN($_, $_ += $_, ...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: calloc + - pattern: realloc + - pattern: reallocf + - pattern: aligned_alloc + - pattern: VirtualAlloc + - patterns: + - pattern: $FUN($_, $N, ...) + - pattern-either: + - pattern-inside: | + $N = $_ + $_; + ... + - pattern-inside: | + $N += $_; + ... + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: calloc + - pattern: realloc + - pattern: reallocf + - pattern: aligned_alloc + - pattern: VirtualAlloc + # calloc (multiplication in 1st arg) + - patterns: + - pattern-either: + - pattern: $FUN($_ * $_, $_) + - pattern: $FUN($_ *= $_, $_) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: calloc + - patterns: + - pattern: $FUN($N, $_) + - pattern-either: + - pattern-inside: | + $N = $_ * $_; + ... + - pattern-inside: | + $N *= $_; + ... + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: calloc + # calloc (sum in 1st arg) + - patterns: + - pattern-either: + - pattern: $FUN($_ + $_, $_) + - pattern: $FUN($_ += $_, $_) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: calloc + - patterns: + - pattern: $FUN($N, $_) + - pattern-either: + - pattern-inside: | + $N = $_ + $_; + ... + - pattern-inside: | + $N += $_; + ... + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: calloc + # buggy input validation (sum) + # consider integer promotion (i.e., uint16_t are not vulnerable on 32-bit systems) + - pattern: $X > $MAX || ($X + $_) > $MAX + - pattern: $X >= $MAX || ($X + $_) > $MAX + - pattern: $X > $MAX || ($X + $_) >= $MAX + - pattern: $X >= $MAX || ($X + $_) >= $MAX + - pattern: $X < $MAX && ($X + $_) < $MAX + - pattern: $X <= $MAX && ($X + $_) < $MAX + - pattern: $X < $MAX && ($X + $_) <= $MAX + - pattern: $X <= $MAX && ($X + $_) <= $MAX + # buggy input validation (multiplication) + # consider integer promotion (i.e., uint16_t are not vulnerable on 32-bit systems) + - pattern: $X > $MAX || ($X * $_) > $MAX + - pattern: $X >= $MAX || ($X * $_) > $MAX + - pattern: $X > $MAX || ($X * $_) >= $MAX + - pattern: $X >= $MAX || ($X * $_) >= $MAX + - pattern: $X < $MAX && ($X * $_) < $MAX + - pattern: $X <= $MAX && ($X * $_) < $MAX + - pattern: $X < $MAX && ($X * $_) <= $MAX + - pattern: $X <= $MAX && ($X * $_) <= $MAX diff --git a/data/rules/0xdea-semgrep-rules/rules/c/memory-address-exposure.yaml b/data/rules/0xdea-semgrep-rules/rules/c/memory-address-exposure.yaml new file mode 100644 index 0000000..37cef34 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/memory-address-exposure.yaml @@ -0,0 +1,77 @@ +rules: + - id: raptor-memory-address-exposure + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Mishandled Sensitive Information + likelihood: LOW + impact: MEDIUM + confidence: LOW + technology: + - c + - cpp + cwe: + - "CWE-209: Generation of Error Message Containing Sensitive Information" + - "CWE-497: Exposure of Sensitive System Information to an Unauthorized Control Sphere" + owasp: + - A04:2025 - Cryptographic Failures + references: + - https://github.com/struct/mms + message: >- + The software generates an error message that includes sensitive + information about its environment, users, or associated data. + In particular, exposure of memory addresses might defeat ASLR. + severity: INFO + languages: + - c + - cpp + # NOTE: `#define` format specifiers are not considered due to Semgrep limitations, perhaps we could implement this with symbolic propagation (Semgrep Pro experimental feature)? + patterns: + - pattern-either: + # format string in 1st arg + - pattern: printf($FMT, ...) + - pattern: vprintf($FMT, ...) + - pattern: wprintf($FMT, ...) + - pattern: vwprintf($FMT, ...) + - pattern: vcprintf($FMT, ...) + - pattern: vcwprintf($FMT, ...) + - pattern: vscprintf($FMT, ...) + - pattern: vscwprintf($FMT, ...) + - pattern: printk($FMT, ...) + - pattern: warn($FMT, ...) + - pattern: vwarn($FMT, ...) + - pattern: warnx($FMT, ...) + - pattern: vwarnx($FMT, ...) + # format string in 2nd arg + - pattern: fprintf($_, $FMT, ...) + - pattern: vfprintf($_, $FMT, ...) + - pattern: fwprintf($_, $FMT, ...) + - pattern: vfwprintf($_, $FMT, ...) + - pattern: sprintf($_, $FMT, ...) + - pattern: vsprintf($_, $FMT, ...) + - pattern: asprintf($_, $FMT, ...) + - pattern: vasprintf($_, $FMT, ...) + - pattern: dprintf($_, $FMT, ...) + - pattern: vdprintf($_, $FMT, ...) + - pattern: wsprintf($_, $FMT, ...) + - pattern: syslog($_, $FMT, ...) + - pattern: vsyslog($_, $FMT, ...) + - pattern: err($_, $FMT, ...) + - pattern: verr($_, $FMT, ...) + - pattern: errx($_, $FMT, ...) + - pattern: verrx($_, $FMT, ...) + - pattern: warnc($_, $FMT, ...) + - pattern: vwarnc($_, $FMT, ...) + # format string in 3rd arg + - pattern: snprintf($_, $_, $FMT, ...) + - pattern: vsnprintf($_, $_, $FMT, ...) + - pattern: swprintf($_, $_, $FMT, ...) + - pattern: vswprintf($_, $_, $FMT, ...) + - pattern: errc($_, $_, $FMT, ...) + - pattern: verrc($_, $_, $FMT, ...) + - metavariable-regex: + metavariable: $FMT + # NOTE: some format string specifiers are not handled + regex: '(".*%\w*x.*"|".*%\w*X.*"|".*%\w*p.*")' diff --git a/data/rules/0xdea-semgrep-rules/rules/c/mismatched-memory-management-cpp.yaml b/data/rules/0xdea-semgrep-rules/rules/c/mismatched-memory-management-cpp.yaml new file mode 100644 index 0000000..e9123ed --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/mismatched-memory-management-cpp.yaml @@ -0,0 +1,67 @@ +rules: + - id: raptor-mismatched-memory-management-cpp + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: LOW + impact: MEDIUM + confidence: LOW + technology: + - cpp + cwe: + - "CWE-762: Mismatched Memory Management Routines" + owasp: + - A06:2025 - Insecure Design + references: + - https://github.com/struct/mms + - https://docs.microsoft.com/en-us/cpp/sanitizers/asan-error-examples + message: >- + The software attempts to return a memory resource to the system, but + it calls a release function that is not compatible with the function + that was originally used to allocate that resource. + Due to inherent limitations of Semgrep, this rule might generate many + false positives and should therefore be customized for your codebase. + severity: INFO + languages: + - cpp + # NOTE: `valloc`, `reallocf`, `aligned_alloc`, and custom wrappers are not covered. + # NOTE: overloaded operators, `VirtualAlloc/VirtualFree`, and `mmap/munmap` are not covered. + pattern-either: + # free + - patterns: + - pattern: free($PTR) + - pattern-not-inside: | + $PTR = malloc(...); + ... + - pattern-not-inside: | + $PTR = calloc(...); + ... + - pattern-not-inside: | + $PTR = realloc(...); + ... + - pattern-not-inside: | + $PTR = strdup(...); + ... + - pattern-not-inside: | + $PTR = strndup(...); + ... + # delete[] + - patterns: + - pattern: delete[]($PTR) + - pattern-not-inside: | + $PTR = new $_[$_]; + ... + # delete + - patterns: + - pattern: delete($PTR) + - pattern-not-inside: | + $PTR = new $_; + ... + - patterns: + - pattern: delete($PTR) + - pattern-inside: | + $PTR = new $_[$_]; + ... diff --git a/data/rules/0xdea-semgrep-rules/rules/c/mismatched-memory-management.yaml b/data/rules/0xdea-semgrep-rules/rules/c/mismatched-memory-management.yaml new file mode 100644 index 0000000..a203052 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/mismatched-memory-management.yaml @@ -0,0 +1,51 @@ +rules: + - id: raptor-mismatched-memory-management + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: LOW + impact: MEDIUM + confidence: LOW + technology: + - c + - cpp + cwe: + - "CWE-762: Mismatched Memory Management Routines" + owasp: + - A06:2025 - Insecure Design + references: + - https://github.com/struct/mms + message: >- + The software attempts to return a memory resource to the system, but + it calls a release function that is not compatible with the function + that was originally used to allocate that resource. + Due to inherent limitations of Semgrep, this rule might generate many + false positives and should therefore be customized for your codebase. + severity: INFO + languages: + - c + - cpp + # NOTE: `valloc`, `reallocf`, `aligned_alloc`, and custom wrappers are not covered. + # NOTE: `VirtualAlloc/VirtualFree` and `mmap/munmap` are not covered. + pattern-either: + # free + - patterns: + - pattern: free($PTR) + - pattern-not-inside: | + $PTR = malloc(...); + ... + - pattern-not-inside: | + $PTR = calloc(...); + ... + - pattern-not-inside: | + $PTR = realloc(...); + ... + - pattern-not-inside: | + $PTR = strdup(...); + ... + - pattern-not-inside: | + $PTR = strndup(...); + ... diff --git a/data/rules/0xdea-semgrep-rules/rules/c/off-by-one.yaml b/data/rules/0xdea-semgrep-rules/rules/c/off-by-one.yaml new file mode 100644 index 0000000..d32d7d8 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/off-by-one.yaml @@ -0,0 +1,107 @@ +rules: + - id: raptor-off-by-one + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-193: Off-by-one Error" + - "CWE-787: Out-of-bounds Write" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + - https://github.com/struct/mms + message: >- + The software calculates or uses an incorrect maximum or minimum value + that is 1 more, or 1 less, than the correct value. + severity: WARNING + languages: + - c + - cpp + # NOTE: C++ `new` operator and some heap allocation functions are not covered. + # NOTE: some string and memory copy functions are not covered. + # NOTE: `memcpy(dst + 1, p, len)` and similar scenarios are not covered. + # NOTE: pattern `{$len=snprintf(_); malloc($len);}` is not covered. + # NOTE: some other ways to create an array are not covered. + # NOTE: refer also to `metavariable-comparison` to catch larger indexes as in https://github.com/parsiya/semgrep-hotspots/blob/main/cpp/arrays-out-of-bounds-access.yaml + # NOTE: refer also to `cpp.lang.security.strings.alloc-strlen.alloc-strlen` and `cpp.lang.security.strings.missing-nul-cpp-string-memcpy.missing-nul-cpp-string-memcpy`. + pattern-either: + # array access + - pattern: $BUF[sizeof($BUF)] = ... + - patterns: + - pattern: $BUF[$LEN] = ... + - pattern-either: + - pattern-inside: | + $_ $BUF[$LEN]; + ... + - pattern-inside: | + $_ $BUF[$LEN] = ...; + ... + - patterns: + - pattern: | + *($BUF + $LEN) = ... + - pattern-either: + - pattern-inside: | + $_ $BUF[$LEN]; + ... + - pattern-inside: | + $_ $BUF[$LEN] = ...; + ... + # two-dimentional array access + - patterns: + - pattern: $BUF[$X][$Y] = ... + - pattern-either: + - pattern-inside: | + $_ $BUF[$_][$Y]; + ... + - pattern-inside: | + $_ $BUF[$X][$_]; + ... + # - pattern: for (<... $I = $NUM ...>; <... $I <= $LEN ...>; <... $I++ ...>) ... + # - pattern: for (<... $I = $NUM ...>; <... $I <= $LEN ...>; <... ++$I ...>) ... + # suspicious loops + # <... $I = 0 ...> is still not supported by Semgrep as of 2025-11-22 + # - pattern: for ($_ $I = $NUM; <... $I <= $LEN ...>; <... $I++ ...>) ... + # - pattern: for ($_ $I = $NUM; <... $I <= $LEN ...>; <... ++$I ...>) ... + # - pattern: while (<... $I <= $LEN ...>) ... + # - pattern: do ... while (<... $I <= $LEN ...>); + # strlen vs. sizeof + - pattern: strlen($SRC) > sizeof($DST) + - pattern: strlen($SRC) <= sizeof($DST) + - pattern: sizeof($DST) < strlen($SRC) + - pattern: sizeof($DST) >= strlen($SRC) + # potential strncat misuse + - patterns: + - pattern: strncat($_, $_, $LEN) + - metavariable-pattern: + metavariable: $LEN + patterns: + - pattern-not: $_ - 1 + # out-of-bound indexing: strlen($STR) con be 0 leading to a -1 index + - pattern: $STR[strlen($STR) - 1] + - patterns: + - pattern-inside: | + $LEN = strlen($STR) - 1; + ... + - pattern: $STR[$LEN] + - patterns: + - pattern-inside: | + $LEN = strlen($STR); + ... + - pattern: $STR[$LEN - 1] + # no space allocated for NUL terminator + - pattern: malloc(strlen($STR)) + - patterns: + - pattern-inside: | + $LEN = strlen($STR); + ... + - pattern: malloc($LEN) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/overlapping-source-destination.yaml b/data/rules/0xdea-semgrep-rules/rules/c/overlapping-source-destination.yaml new file mode 100644 index 0000000..ed218d2 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/overlapping-source-destination.yaml @@ -0,0 +1,80 @@ +rules: + - id: raptor-overlapping-source-destination + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: LOW + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-1260: Improper Handling of Overlap Between Protected Memory Ranges" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://linux.die.net/man/3/sprintf + message: >- + C standards specify that the results are undefined if a call to + copy functions, with the exception of memmove(), would cause copying + to take place between objects that overlap (e.g., if the target + string array and one of the supplied input arguments refer to the + same buffer). + severity: WARNING + languages: + - c + - cpp + # NOTE: `vsprintf`, `vsnprintf`, `swprintf`, and `vswprintf` are not covered. + # NOTE: `strcpy_s`, `strcat_s`, and similar variants are not covered. + pattern-either: + - patterns: + - pattern: $FUN($DST, $DST, ...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + # strcpy + - pattern: strcpy + - pattern: strncpy + - pattern: stpcpy + - pattern: stpncpy + - pattern: strlcpy + - pattern: strscpy + - pattern: wcscpy + - pattern: wcsncpy + - pattern: wcpcpy + - pattern: wcpncpy + - pattern: wcslcpy + - pattern: _mbscpy + - pattern: _mbsncpy + - pattern: _mbpcpy + - pattern: _mbpncpy + - pattern: _mbslcpy + - pattern: strccpy + - pattern: strcadd + - pattern: streadd + - pattern: strecpy + # strcat + - pattern: strcat + - pattern: strncat + - pattern: strlcat + - pattern: wcscat + - pattern: wcsncat + - pattern: wcslcat + - pattern: _mbscat + - pattern: _mbsncat + - pattern: _mbslcat + # memcpy + - pattern: memcpy + - pattern: memccpy + - pattern: wmemcpy + - pattern: mempcpy + - pattern: wmempcpy + # bcopy + - pattern: bcopy + # sprintf + - pattern: sprintf($DST, ..., $DST, ...) + - pattern: snprintf($DST, $_, ..., $DST, ...) diff --git a/data/rules/0xdea-semgrep-rules/rules/c/putenv-stack-var.yaml b/data/rules/0xdea-semgrep-rules/rules/c/putenv-stack-var.yaml new file mode 100644 index 0000000..7f90369 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/putenv-stack-var.yaml @@ -0,0 +1,85 @@ +rules: + - id: raptor-putenv-stack-var + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-686: Function Call With Incorrect Argument Type" + - "CWE-562: Return of Stack Variable Address" + owasp: + - A06:2025 - Insecure Design + references: + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + message: >- + The software calls putenv() with a variable that has a short lifetime, + such as a pointer to an automatic variable allocated on the stack. The + correct behavior is to call putenv() with a static/global string. + severity: INFO + languages: + - c + - cpp + # NOTE: global variables declared outside functions and not explicitly tagged as static are not supported. + # NOTE: multi-dimensional arrays and other ways to create an array are not covered. + # NOTE: multiple pointer assignments and other ways to persist after function returns (e.g., output parameter) are not covered. + patterns: + - pattern: putenv($PTR) + - pattern-either: + # array + - patterns: + - pattern-inside: | + $_ $PTR[$_]; + ... + - pattern-not-inside: | + static $_ $PTR[$_]; + ... + - patterns: + - pattern-inside: | + $_ $PTR[$_] = ...; + ... + - pattern-not-inside: | + static $_ $PTR[$_] = ...; + ... + - patterns: + - pattern-inside: | + $_ $PTR[] = ...; + ... + - pattern-not-inside: | + static $_ $PTR[] = ...; + ... + # pointer to array + - patterns: + - pattern-inside: | + $_ $ARR[$_]; + ... + $PTR = $ARR; + ... + - pattern-not-inside: | + static $_ $ARR[$_]; + ... + - patterns: + - pattern-inside: | + $_ $ARR[$_] = ...; + ... + $PTR = $ARR; + ... + - pattern-not-inside: | + static $_ $ARR[$_] = ...; + ... + - patterns: + - pattern-inside: | + $_ $ARR[] = ...; + ... + $PTR = $ARR; + ... + - pattern-not-inside: | + static $_ $ARR[] = ...; + ... diff --git a/data/rules/0xdea-semgrep-rules/rules/c/regex-dos.yaml b/data/rules/0xdea-semgrep-rules/rules/c/regex-dos.yaml new file mode 100644 index 0000000..764eb7c --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/regex-dos.yaml @@ -0,0 +1,55 @@ +rules: + - id: raptor-regex-dos + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Denial-of-Service (DoS) + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-1333: Inefficient Regular Expression Complexity" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://semgrep.dev/docs/writing-rules/metavariable-analysis + - https://github.com/semgrep/semgrep/pull/4672 + message: >- + The software uses a regular expression with an inefficient, possibly + exponential worst-case computational complexity that consumes excessive + CPU cycles. Poorly constructed regular expressions that exhibit + exponential runtime when fed specifically crafted inputs can cause RegEx + denial of service (ReDoS). + severity: WARNING + languages: + - c + - cpp + # NOTE: the C++ `std::regex` library is not covered. + patterns: + - pattern-either: + - pattern: $FUN($_, $REGEX, $_) + - pattern: | + $STRING = $REGEX; + ... + <... $FUN($_, $STRING, $_) ...>; + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: regcomp + - pattern: regncomp + - pattern: regwcomp + - pattern: regwncomp + - pattern: regcomp_l + - pattern: regncomp_l + - pattern: regwcomp_l + - pattern: regwncomp_l + - metavariable-analysis: + analyzer: redos + metavariable: $REGEX + # improve output readability + - focus-metavariable: $REGEX diff --git a/data/rules/0xdea-semgrep-rules/rules/c/ret-stack-address.yaml b/data/rules/0xdea-semgrep-rules/rules/c/ret-stack-address.yaml new file mode 100644 index 0000000..627ec7f --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/ret-stack-address.yaml @@ -0,0 +1,141 @@ +rules: + - id: raptor-ret-stack-address + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: LOW + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-562: Return of Stack Variable Address" + owasp: + - A06:2025 - Insecure Design + references: + - https://github.com/struct/mms + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + - https://docs.microsoft.com/en-us/cpp/sanitizers/asan-error-examples + - https://rules.sonarsource.com/c/type/Bug/RSPEC-946 + message: >- + A function returns the address of a stack variable, which will cause + unintended program behavior, typically in the form of a crash. + severity: INFO + languages: + - c + - cpp + # NOTE: global variables declared outside functions and not explicitly tagged as static are not supported. + # NOTE: multi-dimensional arrays and other ways to create an array are not covered. + # NOTE: multiple pointer assignments and other ways to persist after function returns (e.g., output parameter) are not covered. + # NOTE: a dedicated rule for C++ types/constructs should be implemented (see `cpp.lang.security.strings.return-c-str.return-c-str` and `cpp.lang.security.containers.std-return-data.std-return-data`). + pattern-either: + - patterns: + - pattern: return $PTR; + - pattern-either: + # array + - patterns: + - pattern-inside: | + $_ $PTR[$_]; + ... + - pattern-not-inside: | + static $_ $PTR[$_]; + ... + - patterns: + - pattern-inside: | + $_ $PTR[$_] = ...; + ... + - pattern-not-inside: | + static $_ $PTR[$_] = ...; + ... + - patterns: + - pattern-inside: | + $_ $PTR[] = ...; + ... + - pattern-not-inside: | + static $_ $PTR[] = ...; + ... + # pointer to array + - patterns: + - pattern-inside: | + $_ $ARR[$_]; + ... + $PTR = $ARR; + ... + - pattern-not-inside: | + static $_ $ARR[$_]; + ... + - patterns: + - pattern-inside: | + $_ $ARR[$_] = ...; + ... + $PTR = $ARR; + ... + - pattern-not-inside: | + static $_ $ARR[$_] = ...; + ... + - patterns: + - pattern-inside: | + $_ $ARR[] = ...; + ... + $PTR = $ARR; + ... + - pattern-not-inside: | + static $_ $ARR[] = ...; + ... + - patterns: + # address of local variable + - pattern: return &$VAR; + - pattern-either: + - patterns: + - pattern-inside: | + $_ $VAR; + ... + - pattern-not-inside: | + static $_ $VAR; + ... + - patterns: + - pattern-inside: | + $_ $VAR = ...; + ... + - pattern-not-inside: | + static $_ $VAR = ...; + ... + - patterns: + - pattern-inside: | + $_ $VAR[$_]; + ... + - pattern-not-inside: | + static $_ $VAR[$_]; + ... + - patterns: + - pattern-inside: | + $_ $VAR[$_] = ...; + ... + - pattern-not-inside: | + static $_ $VAR[$_] = ...; + ... + - patterns: + - pattern-inside: | + $_ $VAR[] = ...; + ... + - pattern-not-inside: | + static $_ $VAR[] = ...; + ... + - patterns: + - pattern-inside: | + $_ * $VAR; + ... + - pattern-not-inside: | + static $_ * $VAR; + ... + - patterns: + - pattern-inside: | + $_ * $VAR = ...; + ... + - pattern-not-inside: | + static $_ * $VAR = ...; + ... diff --git a/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-malloc.yaml b/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-malloc.yaml new file mode 100644 index 0000000..d75ec2b --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-malloc.yaml @@ -0,0 +1,77 @@ +rules: + - id: raptor-unchecked-ret-malloc + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-252: Unchecked Return Value" + - "CWE-690: Unchecked Return Value to NULL Pointer Dereference" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + - https://github.com/struct/mms + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + message: >- + The software does not check the return value from a method or + function, which can prevent it from detecting unexpected states and + conditions. + severity: WARNING + languages: + - c + - cpp + # NOTE: custom wrappers are not covered. + # NOTE: for another approach to NULL pointer dereferencing detection, refer to `cpp.lang.security.memory.null-deref.null-library-function.null-library-function`. + patterns: + - pattern-either: + - pattern: $RET = $FUN(...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: malloc + - pattern: calloc + - pattern: realloc + - pattern: reallocf + - pattern: valloc + - pattern: aligned_alloc + - pattern: strdup + - pattern: strndup + - pattern-not-inside: | + $FUN(...); + ... + if (<... $RET ...>) ... + # no need to explicitly check the following patterns, as the deep expression operator above will already match them + # - pattern-not-inside: | + # $FUN(...); + # ... + # if (<... $RET == NULL ...>) ... + # - pattern-not-inside: | + # $FUN(...); + # ... + # if (<... NULL == $RET ...>) ... + # - pattern-not-inside: | + # $FUN(...); + # ... + # if (<... $RET != NULL ...>) ... + # - pattern-not-inside: | + # $FUN(...); + # ... + # if (<... NULL != $RET ...>) ... + # - pattern-not-inside: | + # $FUN(...); + # ... + # if (<... !$RET ...>) ... + - pattern-not-inside: <... $_ == NULL ...> + - pattern-not-inside: <... NULL == $_ ...> + - pattern-not-inside: <... $_ != NULL ...> + - pattern-not-inside: <... NULL != $_ ...> + - pattern-not-inside: <... !$_ ...> diff --git a/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-scanf.yaml b/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-scanf.yaml new file mode 100644 index 0000000..d4d6cca --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-scanf.yaml @@ -0,0 +1,65 @@ +rules: + - id: raptor-unchecked-ret-scanf + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Other Security + likelihood: MEDIUM + impact: LOW + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-252: Unchecked Return Value" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://codeql.github.com/codeql-query-help/cpp/cpp-missing-check-scanf/ + message: >- + The software does not check the return value from a method or + function, which can prevent it from detecting unexpected states and + conditions. + severity: WARNING + languages: + - c + - cpp + patterns: + - pattern: $FUN(...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: scanf + - pattern: __isoc99_scanf + - pattern: vscanf + - pattern: __isoc99_vscanf + - pattern: fscanf + - pattern: __isoc99_fscanf + - pattern: vfscanf + - pattern: __isoc99_vfscanf + - pattern: sscanf + - pattern: __isoc99_sscanf + - pattern: vsscanf + - pattern: __isoc99_vsscanf + - pattern: wscanf + - pattern: vwscanf + - pattern: fwscanf + - pattern: vfwscanf + - pattern: swscanf + - pattern: vswscanf + - pattern-not-inside: $_ = $FUN(...) + - pattern-not-inside: <... $FUN(...) == $_ ...> + - pattern-not-inside: <... $_ == $FUN(...) ...> + - pattern-not-inside: <... $FUN(...) != $_ ...> + - pattern-not-inside: <... $_ != $FUN(...) ...> + - pattern-not-inside: <... $FUN(...) < $_ ...> + - pattern-not-inside: <... $_ > $FUN(...) ...> + - pattern-not-inside: <... $FUN(...) <= $_ ...> + - pattern-not-inside: <... $_ >= $FUN(...) ...> + - pattern-not-inside: <... $FUN(...) > $_ ...> + - pattern-not-inside: <... $_ < $FUN(...) ...> + - pattern-not-inside: <... $FUN(...) >= $_ ...> + - pattern-not-inside: <... $_ <= $FUN(...) ...> + - pattern-not-inside: return $FUN(...); diff --git a/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-setuid-seteuid.yaml b/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-setuid-seteuid.yaml new file mode 100644 index 0000000..da21f83 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/unchecked-ret-setuid-seteuid.yaml @@ -0,0 +1,49 @@ +rules: + - id: raptor-unchecked-ret-setuid-seteuid + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Improper Authorization + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-252: Unchecked Return Value" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://lwn.net/Articles/451985/ + - https://www.usenix.org/legacy/events/sec02/full_papers/chen/chen.pdf + - https://www.openwall.com/lists/oss-security/2023/12/30/4 + message: >- + The software does not check the return value from a method or + function, which can prevent it from detecting unexpected states and + conditions. If the program calls a function to drop privileges but + does not check the return code to ensure that privileges were + successfully dropped, then the program will continue to operate with + the higher privileges. + severity: WARNING + languages: + - c + - cpp + patterns: + - pattern: $FUN(...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: setuid + - pattern: seteuid + - pattern-not-inside: $_ = $FUN(...) + - pattern-not-inside: <... $FUN(...) == $_ ...> + - pattern-not-inside: <... $_ == $FUN(...) ...> + - pattern-not-inside: <... $FUN(...) != $_ ...> + - pattern-not-inside: <... $_ != $FUN(...) ...> + - pattern-not-inside: <... $FUN(...) < $_ ...> + - pattern-not-inside: <... $_ > $FUN(...) ...> + - pattern-not-inside: <... !$FUN(...) ...> + - pattern-not-inside: return $FUN(...); diff --git a/data/rules/0xdea-semgrep-rules/rules/c/unsafe-ret-snprintf-vsnprintf.yaml b/data/rules/0xdea-semgrep-rules/rules/c/unsafe-ret-snprintf-vsnprintf.yaml new file mode 100644 index 0000000..4a5c51d --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/unsafe-ret-snprintf-vsnprintf.yaml @@ -0,0 +1,48 @@ +rules: + - id: raptor-unsafe-ret-snprintf-vsnprintf + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-131: Incorrect Calculation of Buffer Size" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://lwn.net/Articles/507319/ + - https://g.co/kgs/PCHQjJ + - https://dustri.org/b/playing-with-weggli.html + message: >- + The snprintf() and vsnprintf() functions return the total length of + the string they tried to create. Therefore, this return value can be + larger than the size of the destination buffer. If it is used + unsafely, e.g. as an index to write to the destination buffer, memory + corruption might occur. + severity: WARNING + languages: + - c + - cpp + # NOTE: for another approach that uses taint mode, refer to `cpp.lang.security.strings.snprintf-return-value-length.snprintf-return-value-length`. + patterns: + - pattern-either: + - pattern: $_ = $FUN($_, $_, $FMT, ...) + - pattern: $_ += $FUN($_, $_, $FMT, ...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: snprintf + - pattern: vsnprintf + # swprintf and vswprintf return a negative value if n or more + # widechars were requested to be written, thus behaving more safely + - metavariable-regex: + metavariable: $FMT + # NOTE: some format string specifiers are not handled + regex: '(".*%l?s.*"|".*%S.*"|[a-zA-Z_][a-zA-Z0-9_]*)' diff --git a/data/rules/0xdea-semgrep-rules/rules/c/unsafe-ret-strlcpy-strlcat.yaml b/data/rules/0xdea-semgrep-rules/rules/c/unsafe-ret-strlcpy-strlcat.yaml new file mode 100644 index 0000000..269178c --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/unsafe-ret-strlcpy-strlcat.yaml @@ -0,0 +1,46 @@ +rules: + - id: raptor-unsafe-ret-strlcpy-strlcat + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-131: Incorrect Calculation of Buffer Size" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://lwn.net/Articles/507319/ + - https://g.co/kgs/PCHQjJ + message: >- + The strlcpy() and strlcat() functions return the total length of the + string they tried to create. For strlcpy() that means the length of + the source string. For strlcat() that means the initial length of the + destination string plus the length of of the source string. + Therefore, this return value can be larger than the size of the + destination buffer. If it is used unsafely, e.g. as an index to write + to the destination buffer, memory corruption might occur. + severity: WARNING + languages: + - c + - cpp + patterns: + - pattern-either: + - pattern: $_ = $FUN(...) + - pattern: $_ += $FUN(...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: strlcpy + - pattern: strlcat + - pattern: wcslcpy + - pattern: wcslcat + - pattern: _mbslcpy + - pattern: _mbslcat diff --git a/data/rules/0xdea-semgrep-rules/rules/c/unsafe-strlen.yaml b/data/rules/0xdea-semgrep-rules/rules/c/unsafe-strlen.yaml new file mode 100644 index 0000000..0f2d59c --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/unsafe-strlen.yaml @@ -0,0 +1,52 @@ +rules: + - id: raptor-unsafe-strlen + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Other Security + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-190: Integer Overflow or Wraparound" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://github.com/struct/mms + message: >- + The software performs a calculation that can produce an integer + overflow or wraparound, when the logic assumes that the resulting + value will always be larger than the original value. This can + introduce other weaknesses when the calculation is used for resource + management or execution control. When influenced by the size of an + input it is often easier to overflow a short than an int. + severity: WARNING + languages: + - c + - cpp + patterns: + - pattern-either: + # signed and unsigned short + - pattern: (short $_) = $FUN(...) + - pattern: (short int $_) = $FUN(...) + - pattern: (unsigned short $_) = $FUN(...) + - pattern: (unsigned short int $_) = $FUN(...) + # signed int + # - pattern: (int $LEN) = $FUN(...); + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: strlen + - pattern: lstrlen + - pattern: strnlen + - pattern: wcslen + - pattern: wcsnlen + # - pattern: _mbslen + # - pattern: _mbstrlen + # - pattern: _mbsnlen + # - pattern: _mbstrnlen diff --git a/data/rules/0xdea-semgrep-rules/rules/c/unterminated-string-strncpy.yaml b/data/rules/0xdea-semgrep-rules/rules/c/unterminated-string-strncpy.yaml new file mode 100644 index 0000000..cc61eca --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/unterminated-string-strncpy.yaml @@ -0,0 +1,56 @@ +rules: + - id: raptor-unterminated-string-strncpy + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-170: Improper Null Termination" + - "CWE-126: Buffer Over-read" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://g.co/kgs/PCHQjJ + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + message: >- + If there is no NUL character byte in the first n bytes of the source + string, strncpy() and stpncpy() do not NUL-terminate the destination + buffer. If the program does not explicitly terminate the destination + buffer, this will almost certainly result in information disclosure, + and possibly a buffer overflow condition. + severity: WARNING + languages: + - c + - cpp + # NOTE: other functions that do not `NUL`-terminate the destination buffer are not covered (e.g., `read`, `readlink`, `fread`, `memcpy`, etc.). + # NOTE: try to avoid false positives when `sizeof(dest) - 1` is used and in other similar cases. + patterns: + - pattern: $FUN(...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: strncpy + - pattern: stpncpy + - pattern: wcsncpy + - pattern: wcpncpy + - pattern: _mbsncpy + - pattern-not-inside: | + $FUN($DST, $_, $_); + ... + $DST[$_] = NULL; + - pattern-not-inside: | + $FUN($DST, $_, $_); + ... + $DST[$_] = '\0'; + - pattern-not-inside: | + $FUN($DST, $_, $_); + ... + $DST[$_] = 0; diff --git a/data/rules/0xdea-semgrep-rules/rules/c/use-after-free.yaml b/data/rules/0xdea-semgrep-rules/rules/c/use-after-free.yaml new file mode 100644 index 0000000..594b494 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/use-after-free.yaml @@ -0,0 +1,63 @@ +rules: + - id: raptor-use-after-free + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-416: Use After Free" + owasp: + - A10:2025 - Mishandling of Exceptional Conditions + references: + - https://github.com/struct/mms + - https://www.sei.cmu.edu/downloads/sei-cert-c-coding-standard-2016-v01.pdf + - https://docs.microsoft.com/en-us/cpp/sanitizers/asan-error-examples + message: >- + The use of previously-freed memory can have any number of adverse + consequences, ranging from the corruption of valid data to the + execution of arbitrary code, depending on the instantiation and + timing of the flaw. The simplest way data corruption may occur + involves the system's reuse of the freed memory. + severity: ERROR + languages: + - c + - cpp + # NOTE: C++ `delete` and `delete[]` operators are not covered (refer also to `cpp.lang.security.use-after-free.*`). + # NOTE: `realloc` is not covered. + patterns: + - pattern-either: + # generic use after free + - pattern: $PTR->$_ + - pattern: $PTR[$_] + - pattern: (*$PTR) + # use after free in return + - pattern: return $PTR; + # use after free in function calls + - patterns: + - pattern-either: + - pattern: $FUN(..., <... $PTR ...>, ...) + # no need to explicitly check the following patterns, as the deep expression operator above will already match them + # - pattern: $FUN(..., <... $PTR->$_ ...>, ...) + # - pattern: $FUN(..., <... $PTR[$_] ...>, ...) + # - pattern: $FUN(..., <... (*$PTR) ...>, ...) + # - pattern: $FUN(..., <... (*$PTR).$_ ...>, ...) + - pattern: $PTR->$FUN(...) + # filter out double free instances + - metavariable-pattern: + metavariable: $FUN + patterns: + - pattern-not: free + - pattern-inside: free($PTR); ... + - pattern-not-inside: | + free($PTR); + ... + $PTR = ...; + ... diff --git a/data/rules/0xdea-semgrep-rules/rules/c/use-of-source-size-in-copy.yaml b/data/rules/0xdea-semgrep-rules/rules/c/use-of-source-size-in-copy.yaml new file mode 100644 index 0000000..938939d --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/use-of-source-size-in-copy.yaml @@ -0,0 +1,100 @@ +rules: + - id: raptor-use-of-source-size-in-copy + metadata: + author: Marco Ivaldi + category: security + subcategory: + - vuln + vulnerability_class: Memory Issues + likelihood: MEDIUM + impact: HIGH + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-806: Buffer Access Using Size of Source Buffer" + owasp: + - A06:2025 - Insecure Design + references: + - https://github.com/0xdea/advisories/blob/master/2020-07-solaris-whodo-w.txt + - https://dustri.org/b/playing-with-weggli.html + message: >- + The software uses the size of a source buffer when reading from or + writing to a destination buffer, which may cause it to access memory + that is outside of the bounds of the buffer. + severity: ERROR + languages: + - c + - cpp + # NOTE: some copy functions such as `memccpy`, `bcopy`, and `strcpy_s` are not covered. + # NOTE: `strlen` before the copy function is not covered. + # NOTE: structs are not covered. + pattern-either: + # size of array + - patterns: + - pattern: $FUN($DST, $SRC, $LEN) + - pattern-not: $FUN($DST, "...", $LEN) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: strncpy + - pattern: stpncpy + - pattern: strlcpy + - pattern: strscpy + - pattern: memcpy + - pattern: memmove + - pattern: wmemcpy + - pattern: wmemmove + - pattern: mempcpy + - pattern: wmempcpy + - pattern-either: + - patterns: + - pattern-inside: | + $_ $SRC[$LEN]; + ... + - pattern-not-inside: | + $_ $DST[$LEN]; + ... + - pattern-not-inside: | + $_ $DST[$LEN] = ...; + ... + - patterns: + - pattern-inside: | + $_ $SRC[$LEN] = ...; + ... + - pattern-not-inside: | + $_ $DST[$LEN]; + ... + - pattern-not-inside: | + $_ $DST[$LEN] = ...; + ... + # sizeof operator, strlen, and similar + - patterns: + - pattern: $FUN($DST, $SRC, <... $SRC ...>) + - pattern-not: $FUN($DST, "...", <... $SRC ...>) + # - pattern-not: $FUN($DST, $SRC, <... $DST ...>) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: strncpy + - pattern: stpncpy + - pattern: strlcpy + - pattern: strscpy + - pattern: memcpy + - pattern: memmove + - pattern: wmemcpy + - pattern: wmemmove + - pattern: mempcpy + - pattern: wmempcpy + # snprintf + - patterns: + - pattern: $FUN($DST, <... $SRC ...>, ..., $SRC, ...) + # - pattern-not: $FUN($DST, <... $DST ...>, ..., $SRC, ...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + - pattern: snprintf + - pattern: vsnprintf + - pattern: swprintf + - pattern: vswprintf diff --git a/data/rules/0xdea-semgrep-rules/rules/c/write-into-stack-buffer.yaml b/data/rules/0xdea-semgrep-rules/rules/c/write-into-stack-buffer.yaml new file mode 100644 index 0000000..5be6610 --- /dev/null +++ b/data/rules/0xdea-semgrep-rules/rules/c/write-into-stack-buffer.yaml @@ -0,0 +1,189 @@ +rules: + - id: raptor-write-into-stack-buffer + metadata: + author: Marco Ivaldi + category: security + subcategory: + - audit + vulnerability_class: Memory Issues + likelihood: LOW + impact: LOW + confidence: MEDIUM + technology: + - c + - cpp + cwe: + - "CWE-121: Stack-based Buffer Overflow" + owasp: + - A08:2025 - Software or Data Integrity Failures + references: + - https://github.com/googleprojectzero/weggli + message: >- + The software directly writes into a stack buffer. This might lead to + a stack-based buffer overflow. + severity: INFO + languages: + - c + - cpp + # NOTE: some functions (e.g., the `scanf` family) and some other ways to write into a stack buffer are not covered. + # NOTE: `memcpy_s`, `memmove_s`, `memset_explicit`, and similar variants are not covered. + # NOTE: `strcpy_s`, `strcat_s`, and similar variants are not covered. + # NOTE: global variables declared outside functions and not explicitly tagged as `static` are not supported. + # NOTE: based on `semgrep --time` output, processing of this rule seems to be particularly slow; consider disabling it, if you don't need it. + pattern-either: + # buffer in 1st arg (buffer to buffer copy functions) + - patterns: + - pattern: $FUN($BUF, ...) + - pattern-inside: | + $_ $BUF[$_]; + ... + - pattern-not-inside: | + static $_ $BUF[$_]; + ... + - pattern-not: $FUN($BUF, "...", ...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + # strcpy + - pattern: strcpy + - pattern: strncpy + - pattern: stpcpy + - pattern: stpncpy + - pattern: strlcpy + - pattern: strscpy + - pattern: wcscpy + - pattern: wcsncpy + - pattern: wcpcpy + - pattern: wcpncpy + - pattern: wcslcpy + - pattern: _mbscpy + - pattern: _mbsncpy + - pattern: _mbpcpy + - pattern: _mbpncpy + - pattern: _mbslcpy + - pattern: strccpy + - pattern: strcadd + - pattern: streadd + - pattern: strecpy + # strcat + - pattern: strcat + - pattern: strncat + - pattern: strlcat + - pattern: wcscat + - pattern: wcsncat + - pattern: wcslcat + - pattern: _mbscat + - pattern: _mbsncat + - pattern: _mbslcat + # memcpy/memset + - pattern: memcpy + - pattern: memccpy + - pattern: mempcpy + - pattern: memmove + - pattern: memset + - pattern: wmemcpy + - pattern: wmempcpy + - pattern: wmemmove + - pattern: wmemset + # buffer in 1st arg (sprintf functions) + - patterns: + - pattern: $FUN($BUF, $FMT, ...) + - pattern-inside: | + $_ $BUF[$_]; + ... + - pattern-not-inside: | + static $_ $BUF[$_]; + ... + - metavariable-regex: + metavariable: $FMT + # some format string specifiers are not handled + regex: '(".*%l?s.*"|".*%S.*"|[a-zA-Z_][a-zA-Z0-9_]*)' + - metavariable-pattern: + metavariable: $FUN + pattern-either: + # sprintf + - pattern: sprintf + - pattern: vsprintf + # buffer in 1st arg (snprintf functions) + - patterns: + - pattern: $FUN($BUF, $_, $FMT, ...) + - pattern-inside: | + $_ $BUF[$_]; + ... + - pattern-not-inside: | + static $_ $BUF[$_]; + ... + - metavariable-regex: + metavariable: $FMT + # some format string specifiers are not handled + regex: '(".*%l?s.*"|".*%S.*"|[a-zA-Z_][a-zA-Z0-9_]*)' + - metavariable-pattern: + metavariable: $FUN + pattern-either: + # snprintf + - pattern: snprintf + - pattern: vsnprintf + - pattern: swprintf + - pattern: vswprintf + # buffer in 1st arg (other functions) + - patterns: + - pattern: $FUN($BUF, ...) + - pattern-inside: | + $_ $BUF[$_]; + ... + - pattern-not-inside: | + static $_ $BUF[$_]; + ... + - metavariable-pattern: + metavariable: $FUN + pattern-either: + # get + - pattern: gets + - pattern: fgets + - pattern: getwd + - pattern: getcwd + # stdio + - pattern: fread + # buffer in 2nd arg (buffer to buffer copy functions) + - patterns: + - pattern: $FUN($_, $BUF, ...) + - pattern-inside: | + $_ $BUF[$_]; + ... + - pattern-not-inside: | + static $_ $BUF[$_]; + ... + - pattern-not: $FUN("...", $BUF, ...) + - metavariable-pattern: + metavariable: $FUN + pattern-either: + # bcopy + - pattern: bcopy + # buffer in 2nd arg (other functions) + - patterns: + - pattern: $FUN($_, $BUF, ...) + - pattern-inside: | + $_ $BUF[$_]; + ... + - pattern-not-inside: | + static $_ $BUF[$_]; + ... + - metavariable-pattern: + metavariable: $FUN + pattern-either: + # read + - pattern: read + - pattern: pread + # recv + - pattern: recv + - pattern: recvfrom + # assignment (too many false positives) + # - patterns: + # - pattern: $BUF[$_] = ...; + # - pattern: $BUF[] = ...; + # - pattern-inside: | + # $_ $BUF[$_]; + # ... + # - pattern-not-inside: | + # static $_ $BUF[$_]; + # ... diff --git a/data/rules/MANIFEST.json b/data/rules/MANIFEST.json new file mode 100644 index 0000000..0a2a805 --- /dev/null +++ b/data/rules/MANIFEST.json @@ -0,0 +1,4271 @@ +{ + "version": 1, + "generated_by": "python -m anvil_eval.vendor_rules", + "selection_sha256": "7b86bf58da7eaca7d0a19ec9a96fef3fe2b8ace672c4a8b7647e3ff5c236e729", + "licences": [ + { + "path": "0xdea-semgrep-rules/LICENSE", + "sha256": "1f9dca8eb03ff3c87d77694e09b36a06485158a161c3614b0d45277bc6ddd767" + }, + { + "path": "gitlab-sast-rules/LICENSE", + "sha256": "33acf42afdd03163d75fe856cb431e70710e0b5deede3362e6d1c4697cfe1c6a" + }, + { + "path": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt", + "sha256": "09e8a9bcec8067104652c168685ab0931e7868f9c8284b66f5ae6edae5f1130b" + }, + { + "path": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt", + "sha256": "2be0a81f30d2b73cb3b5182b969175245042158735fcc8af133929212c772e78" + } + ], + "rules": [ + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/csharp/crypto/rule-WeakCipherMode.yml", + "blob_sha1": "a5ce3fe55c0ba5e990b1a9a05da6dae9eb1cc45d", + "sha256": "bb8a0f1ae5effbbacdb921b5f1875d9d8a3198133898cb12ed6ed42e77a663a1", + "ids": [ + "csharp_crypto_rule-WeakCipherMode" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "csharp" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-WeakCipherMode.cs: License: MIT Copyright (c) 2022-Present GitLab B.V.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/blocklist/rule-blocklist-des.yml", + "blob_sha1": "59e9c424444b1aa42cdb0bddf335ad796688219b", + "sha256": "3847da3d5b77b31882aab293319871a852c99b534bb322c3a602331912e77287", + "ids": [ + "go_blocklist_rule-blocklist-des" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-blocklist-des.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/blocklist/rule-blocklist-md5.yml", + "blob_sha1": "d1e9d5f967371587e56104286218156274a7ba1f", + "sha256": "1284f0ec904497a6ca6e2bf3eb44c87ad28a1480ddf12c3be5496885a1659a66", + "ids": [ + "go_blocklist_rule-blocklist-md5" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-blocklist-md5.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/blocklist/rule-blocklist-rc4.yml", + "blob_sha1": "21ec66d2e7e673a531f6bbed1699933a3a4d19c8", + "sha256": "93dd91d71c4733c59b4528fb70ab578681be4d521fcc78ea97db5d0e920fef6e", + "ids": [ + "go_blocklist_rule-blocklist-rc4" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-blocklist-rc4.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/blocklist/rule-blocklist-sha1.yml", + "blob_sha1": "edb06d38496209a680eb6b9cb7a612620c2513ba", + "sha256": "838e84783441f8f293a2f96da0421ea1aa1fc81276d741e36d9350c516067f1c", + "ids": [ + "go_blocklist_rule-blocklist-sha1" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "# License: Apache 2.0 (c) gosec; its GitLab companion rule-blocklist-sha1.go: License: Apache 2.0", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/crypto/rule-badtlssettings.yml", + "blob_sha1": "80b8936e63cf5eb8b8230a8b2c54c2b5af76b084", + "sha256": "293cbc1ba2b8cbcf2357392f02ad2fc5b8a71c91e2b2481145671d75bd3e1568", + "ids": [ + "go_crypto_rule-badtlssettings" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-badtlssettings.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/crypto/rule-insecure-ignore-host-key.yml", + "blob_sha1": "fd3d6a7e8f42e1cbc41b96a3a1b8276570c2b328", + "sha256": "8b44db76de65512904ba20ea1eac327c682d50dfb2d0d8551e2915c8b417dc34", + "ids": [ + "go_crypto_rule-insecure-ignore-host-key" + ], + "cwes": [ + "CWE-322" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-insecure-ignore-host-key.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/crypto/rule-tlsversion.yml", + "blob_sha1": "95b23d2336278fcd856d02620889c3b4a1c8981f", + "sha256": "447cfe992de2ef972ae8ebda534653290c23d12a0860385401be779634891d9d", + "ids": [ + "go_crypto_rule-tlsversion" + ], + "cwes": [ + "CWE-757" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-tlsversion.go: License: Apache 2.0 (c) gosec; the rule's own header says License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/crypto/rule-weakkeystrength.yml", + "blob_sha1": "4b88f37ba2d4f793a341473bdfd78a22fd27fe06", + "sha256": "4bc5ea4080746745677533317f4b8a15c02561ee2462bad4b1a278ed0cdfa137", + "ids": [ + "go_crypto_rule-weakkeystrength" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-weakkeystrength.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/crypto/rule-weakrandsource.yml", + "blob_sha1": "1b6e8bb9726cd996202e5b2745757d54c7e748f0", + "sha256": "219f254406b3b425ee7e256bb3a42949caf3b45151f9608b29183d29dcdb536a", + "ids": [ + "go_crypto_rule-weakrandsource" + ], + "cwes": [ + "CWE-338" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-weakrandsource.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/file_permissions/rule-fileperm.yml", + "blob_sha1": "a6e5bc805b4499b3f5743e131475728bbd4f76c9", + "sha256": "fce51d68dce96d0472371290d2ad5177fedc82c4a04058a4007ea81203a2e894", + "ids": [ + "go_file-permissions_rule-fileperm" + ], + "cwes": [ + "CWE-732" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-fileperm.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/file_permissions/rule-mkdir.yml", + "blob_sha1": "87767da1f29aa14e28cae0e6c1d1c6e7d8177a99", + "sha256": "841aa14d55ee79bdf183beb4cc41033b79eed20d0631935703bee9558ee30ae6", + "ids": [ + "go_file-permissions_rule-mkdir" + ], + "cwes": [ + "CWE-732" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-mkdir.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/filesystem/rule-decompression-bomb.yml", + "blob_sha1": "577e35fdab7cc0b7c294a0d852588f0f2154ee00", + "sha256": "219e19deb11a6e743a977ef91a98120554793b08bfc2f6a942ee38336401ad0a", + "ids": [ + "go_filesystem_rule-decompression-bomb" + ], + "cwes": [ + "CWE-409" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-decompression-bomb.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/filesystem/rule-fileread.yml", + "blob_sha1": "31646692398f54eadcf659deadc3162f27e06a83", + "sha256": "c14e6e3f658c59a4b62081768fe0d9ff4efaab151d9e84975e6411045e7aacec", + "ids": [ + "go_filesystem_rule-fileread" + ], + "cwes": [ + "CWE-22" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-fileread.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/filesystem/rule-httprootdir.yml", + "blob_sha1": "8f9917ee41a02323ace4af85a8cc253a0b5a4de7", + "sha256": "b1835648cdf63bcaa1ff0197a1ecc95015bf8f9f343e66d07d122e1ea9e58f4d", + "ids": [ + "go_filesystem_rule-httprootdir" + ], + "cwes": [ + "CWE-552" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "# License: Apache 2.0 (c) gosec; its GitLab companion rule-httprootdir.go: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/filesystem/rule-poorwritepermissions.yml", + "blob_sha1": "8adc1de9d25e4ab7dc373e20bbd3a2796631fc57", + "sha256": "23c9300589ca87b5dca081451151d705234b5d21a50613106b679b26c6149f4f", + "ids": [ + "go_filesystem_rule-poorwritepermissions" + ], + "cwes": [ + "CWE-276" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-poorwritepermissions.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/filesystem/rule-tempfiles.yml", + "blob_sha1": "1170b4bb00ff63b2929dc51440aae193ad7fd5f1", + "sha256": "ea993a23913efe2b62caf0904221c86403d8cb4952d1d9e53fb4edf617b398a1", + "ids": [ + "go_filesystem_rule-tempfiles" + ], + "cwes": [ + "CWE-378" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-tempfiles.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/filesystem/rule-ziparchive.yml", + "blob_sha1": "57cb0709492e8b21829467ea7f4d09f8e0d0d29c", + "sha256": "61744a438d8cf45470791ffac1bb6cc922a9165c0ce2a7a8279d54d335211e28", + "ids": [ + "go_filesystem_rule-ziparchive" + ], + "cwes": [ + "CWE-22" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-ziparchive.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/http/rule-http-serve.yml", + "blob_sha1": "9a7424e9cb14a3d12cc0ba7eb19a33352a69ef60", + "sha256": "87ae144671fd1bfd1a824e54a9731f369cfb22f68934c3fb333428e50ccb916f", + "ids": [ + "go_http_rule-http-serve" + ], + "cwes": [ + "CWE-770" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-http-serve.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/injection/rule-ssrf.yml", + "blob_sha1": "414accdc29495705b51c42b28a58f4d8e951c22a", + "sha256": "dfe372da4c399d512cf19dcb118b613a10c3b0abe8a574c3994048ec6d98437e", + "ids": [ + "go_injection_rule-ssrf" + ], + "cwes": [ + "CWE-918" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-ssrf.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/injection/rule-template-injection.yml", + "blob_sha1": "83cbef3686706856848c28bc41f7511911683a36", + "sha256": "ce15c5ef530e329743fe227fc45e81cdb36c1854943a6f6739544cec809683bc", + "ids": [ + "go_injection_rule-template-injection" + ], + "cwes": [ + "CWE-79" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-template-injection.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/leak/rule-pprof-endpoint.yml", + "blob_sha1": "a39f8b4e17c030a6b449989c865d438e116afff2", + "sha256": "0ae795faedd0501a871637b516770eb8599660e637c292b2ae519abbb8591566", + "ids": [ + "go_leak_rule-pprof-endpoint" + ], + "cwes": [ + "CWE-489" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-pprof-endpoint.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/memory/rule-integer-overflow.yml", + "blob_sha1": "b8d48df4ef29754204ec8bd001d3b31569d73100", + "sha256": "60379e31b48631dc5bbcb7270d636e091c566139b55d75d3e5bafc8e62f84d5d", + "ids": [ + "go_memory_rule-integer-overflow" + ], + "cwes": [ + "CWE-190" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-integer-overflow.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/memory/rule-memoryaliasing.yml", + "blob_sha1": "58142036e7f8ac7d97727cfc54ee11d9149765ae", + "sha256": "06bfad2f4d4592dd7e8b290cb083ad592d2cb1e6c575b38ccff9bdd0d050a04b", + "ids": [ + "go_memory_rule-memoryaliasing" + ], + "cwes": [ + "CWE-118" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-memoryaliasing.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/network/rule-bind-to-all-interfaces.yml", + "blob_sha1": "534eb44ae5b3c4031531e4dcc52783be0102195a", + "sha256": "bbad7ae4b87e409d3c5a536611c00225abdbcc776aab949df175ca23be004bd5", + "ids": [ + "go_network_rule-bind-to-all-interfaces" + ], + "cwes": [ + "CWE-1327" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-bind-to-all-interfaces.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/sql/rule-concat-sqli.yml", + "blob_sha1": "3997ad3edc7fc8563696bec258b17db786bc4080", + "sha256": "faeaf36a51298ab0f4f56a77bdbbac9a5f97d4f2fe35cfb05cc69778eb81b760", + "ids": [ + "go_sql_rule-concat-sqli" + ], + "cwes": [ + "CWE-89" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-concat-sqli.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/subproc/rule-subproc.yml", + "blob_sha1": "88937e89be39b756e27a0c911b31d64824fbd13f", + "sha256": "c920efef89fc373ab991a594eb0f92768fced69f6bd46a569bfad271a30233fb", + "ids": [ + "go_subproc_rule-subproc" + ], + "cwes": [ + "CWE-78" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-subproc.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/go/unsafe/rule-unsafe.yml", + "blob_sha1": "f6fecc9a3f93fd333cd12ec4161f38d9846460c0", + "sha256": "25c5a206ed1149c502b5ae00350e4295746cd8ef8ac464f4f92ee6fd7aea339a", + "ids": [ + "go_unsafe_rule-unsafe" + ], + "cwes": [ + "CWE-242" + ], + "languages": [ + "go" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from gosec: its GitLab companion rule-unsafe.go: License: Apache 2.0 (c) gosec; the rule's own header says Apache 2.0 (c) gosec", + "licence_file": "gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/cookie/rule-RequestParamToHeader.yml", + "blob_sha1": "a4a01e18c799a8914e4354eb04a4215efd861079", + "sha256": "0d40a67c62f374fe0462d8ba3e371a9515b16aefc78f43f921eca7643aa88b1b", + "ids": [ + "java_cookie_rule-RequestParamToHeader" + ], + "cwes": [ + "CWE-113" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-RequestParamToHeader.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/cors/rule-PermissiveCORSInjection.yml", + "blob_sha1": "ba997e4b4155bf1e2018e8cc7f494581b1397cab", + "sha256": "c92bd81dcef0007d36dde4d887ce718f65a1c82696596daff41d8d6d7408086b", + "ids": [ + "java_cors_rule-PermissiveCORSInjection" + ], + "cwes": [ + "CWE-942" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-PermissiveCORSInjection.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/crypto/rule-CipherDESInsecure.yml", + "blob_sha1": "311f713961ee29c34ad1e94faf43c9bee4a854d7", + "sha256": "aa785226525d9e05d59b3a8b6679c3a6ccb51b1549c95a35808c077f40d69913", + "ids": [ + "java_crypto_rule-CipherDESInsecure" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherDESInsecure.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/crypto/rule-CipherDESedeInsecure.yml", + "blob_sha1": "7a7c4bcf6b361adbe3452d369945850f0b314459", + "sha256": "cacb2b30067ce28e206777e051749bbef6c9de4ff80e05694cf4ea060b2eef3e", + "ids": [ + "java_crypto_rule-CipherDESedeInsecure" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherDESedeInsecure.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/crypto/rule-CipherECBMode.yml", + "blob_sha1": "88350d2e9295cf97dae6be71ab38730608b43758", + "sha256": "14ac22e6185951e9d45f890f833929dc3fef7440f442a46aacaa61e73774a3c5", + "ids": [ + "java_crypto_rule-CipherECBMode" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherECBMode.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/crypto/rule-CipherIntegrity.yml", + "blob_sha1": "95f99bc82a604efcb5bbd46d4df8701d2e407146", + "sha256": "4ee989ddb4958a14a2ca7ff9df8bd48503db72710341de2869fd24b83c99b91a", + "ids": [ + "java_crypto_rule-CipherIntegrity" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherIntegrity.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/crypto/rule-CipherPaddingOracle.yml", + "blob_sha1": "c15ac4d1241895b465a880944e7fbb28e171acb3", + "sha256": "54a56985718cd56bb21eb16f3dbb5982d9c111ea42cc7c4c39781bf394457570", + "ids": [ + "java_crypto_rule-CipherPaddingOracle" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherPaddingOracle.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/crypto/rule-WeakTLSProtocolVersion.yml", + "blob_sha1": "4bd5fdf888cf54601f4c0db558fa9c0f63bb5681", + "sha256": "5a818f90e3ab6fb68f38de3687d0d80f3c979e85db14613def8f0e8e7d9885ae", + "ids": [ + "java_crypto_rule-WeakTLSProtocolVersion" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-WeakTLSProtocolVersion.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/inject/rule-ELInjection.yml", + "blob_sha1": "bfd56ae1c720a0cf7cd20d38326a0e4fae0b6566", + "sha256": "ed18e62ac44e768c8eb03d0280a0567e24b73170355cb206091e2ca20109b158", + "ids": [ + "java_inject_rule-ELInjection" + ], + "cwes": [ + "CWE-917" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-ELInjection.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/password/rule-ConstantDBPassword.yml", + "blob_sha1": "43969aca24716cffcc66af38a4e7ccde40f48d4a", + "sha256": "7cf5a7dd8107bba7c57e8f868ad44e0c9a945a0d1ee3197254af77862cde7350", + "ids": [ + "java_password_rule-ConstantDBPassword" + ], + "cwes": [ + "CWE-259" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-ConstantDBPassword.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/password/rule-EmptyDBPassword.yml", + "blob_sha1": "805888590ad0488e795c7056245a26eb61bc151a", + "sha256": "6d5650e0a70cc8fd8a0c760b12c82e0f2f96eb9b0aec142bfea7ac896853ff87", + "ids": [ + "java_password_rule-EmptyDBPassword" + ], + "cwes": [ + "CWE-306" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-EmptyDBPassword.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/java/xml/rule-XmlDecoder.yml", + "blob_sha1": "9df19bbdae3376133e118edf5ec4f0ca7b534f31", + "sha256": "4acabd60252b9c8e3cd88db36d82fee5f00e996cdd67e013c0fabdd79f167e77", + "ids": [ + "java_xml_rule-XmlDecoder" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-XmlDecoder.java: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/buf/rule-buffer-noassert-read.yml", + "blob_sha1": "faf9abeaccf27f45f66b0d3fbed59f1e44fce363", + "sha256": "5ed51fdc61c271d8d2856ec1f62f42257bddf5b0f8e2fd5a14dd164c68f49cac", + "ids": [ + "javascript_buf_rule-buffer-noassert-read" + ], + "cwes": [ + "CWE-125" + ], + "languages": [ + "javascript" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-buffer-noassert-read.js states no licence", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/buf/rule-buffer-noassert-write.yml", + "blob_sha1": "cc941bad92a0ca812f6bb3e9776a6173921da566", + "sha256": "44267b8f91c7f73a35e5768b3f268aa63fdb5d09c38afde215c5bbd9d132279e", + "ids": [ + "javascript_buf_rule-buffer-noassert-write" + ], + "cwes": [ + "CWE-787" + ], + "languages": [ + "javascript" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-buffer-noassert-write.js states no licence", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/buf/rule-detect-new-buffer.yml", + "blob_sha1": "8c35dc6ab4ad5539e02f36e08e687791ecde14f2", + "sha256": "4b1ca91bd91b36e70b6c92c2532992abf37759c80027fff4e786d34acd5564f2", + "ids": [ + "javascript_buf_rule-detect-new-buffer" + ], + "cwes": [ + "CWE-770" + ], + "languages": [ + "javascript", + "typescript" + ], + "licence": "MIT", + "licence_evidence": "derived from JS Foundation: its GitLab companion rule-detect-new-buffer.js: License: MIT (c) JS Foundation and other contributors, https://js.foundation; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/dos/rule-non-literal-regexp.yml", + "blob_sha1": "0f813b76713ad7077acf2a92dcade78c46c2cdd9", + "sha256": "5178adc3e41175c46b4f0b377cfc87de5bb5d7580af75a32bbfb183f5feb22ff", + "ids": [ + "javascript_dos_rule-non-literal-regexp" + ], + "cwes": [ + "CWE-185" + ], + "languages": [ + "javascript", + "typescript" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-non-literal-regexp.js: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/eval/rule-eval-with-expression.yml", + "blob_sha1": "f4df5391f93c8fe529860cacf6830c16263c99e8", + "sha256": "6a60110e2d2a7f22d9c4a69b09d2d489e151da40ad5c8aeb6a55401791faf5ed", + "ids": [ + "javascript_eval_rule-eval-with-expression" + ], + "cwes": [ + "CWE-95" + ], + "languages": [ + "javascript", + "typescript" + ], + "licence": "MIT", + "licence_evidence": "derived from JS Foundation: its GitLab companion rule-eval-with-expression.js: License: MIT (c) JS Foundation and other contributors, https://js.foundation; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/pathtraversal/rule-non-literal-fs-filename.yml", + "blob_sha1": "c0d8dcf2117faa330664145a29f3b3e84e5650f4", + "sha256": "429484d0b1c907ec22f6416c97ca8b0a46d8e8a23683384de0d0c4e6b0344169", + "ids": [ + "javascript_pathtraversal_rule-non-literal-fs-filename" + ], + "cwes": [ + "CWE-22" + ], + "languages": [ + "typescript", + "javascript" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-non-literal-fs-filename.js: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/random/rule-pseudo-random-bytes.yml", + "blob_sha1": "a984bf2cb90633d5eea95829bd5769a5100cb671", + "sha256": "44b32800f17196b53b965e1be11b787a521cdf5ce164249c0a2e2d4604c0b870", + "ids": [ + "javascript_random_rule-pseudo-random-bytes" + ], + "cwes": [ + "CWE-338" + ], + "languages": [ + "javascript", + "typescript" + ], + "licence": "MIT", + "licence_evidence": "derived from JS Foundation: its GitLab companion rule-pseudo-random-bytes.js: License: MIT (c) JS Foundation and other contributors, https://js.foundation; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/react/rule-dangerouslysetinnerhtml.yml", + "blob_sha1": "dfbbafa2a6564dca2463ece6ac320837076f7be7", + "sha256": "1a43ece6a249e7486bfb156a4bfc0ea7043d477e36d52442b07fd9291ae60e68", + "ids": [ + "javascript_react_rule-dangerouslysetinnerhtml" + ], + "cwes": [ + "CWE-79" + ], + "languages": [ + "typescript", + "javascript" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-dangerouslysetinnerhtml.js: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/require/rule-non-literal-require.yml", + "blob_sha1": "b81110bbc62930a35a374dadafb8e6336675b11d", + "sha256": "e464056a07762ff78332adc779ec8bf558a09304d7d09881f750135803fd9564", + "ids": [ + "javascript_require_rule-non-literal-require" + ], + "cwes": [ + "CWE-95" + ], + "languages": [ + "javascript", + "typescript" + ], + "licence": "MIT", + "licence_evidence": "derived from JS Foundation: its GitLab companion rule-non-literal-require.js: License: MIT (c) JS Foundation and other contributors, https://js.foundation; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/timing/rule-possible-timing-attacks.yml", + "blob_sha1": "4affb2f6a133d25d19fc8e2d09cccb2c2632d87b", + "sha256": "dde4f910f55979cba09d1994626f17d1dd27f4e0ba89199dcb0428580bb158bf", + "ids": [ + "javascript_timing_rule-possible-timing-attacks" + ], + "cwes": [ + "CWE-208" + ], + "languages": [ + "javascript" + ], + "licence": "MIT", + "licence_evidence": "derived from JS Foundation: its GitLab companion rule-possible-timing-attacks.js: License: MIT (c) JS Foundation and other contributors, https://js.foundation; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/javascript/xss/rule-mustache-escape.yml", + "blob_sha1": "429504fc6346d685e6136d9eeaeac8f55cd619ab", + "sha256": "58ca5a063f5f919333c79d7e8adb4f16575ae963fede25589ea0f32fe2b36549", + "ids": [ + "javascript_xss_rule-mustache-escape" + ], + "cwes": [ + "CWE-79" + ], + "languages": [ + "javascript", + "typescript" + ], + "licence": "MIT", + "licence_evidence": "derived from JS Foundation: its GitLab companion rule-mustache-escape.js: License: MIT (c) JS Foundation and other contributors, https://js.foundation; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/bind_all_interfaces/rule-general-bindall-interfaces.yml", + "blob_sha1": "b642e26fc3919a71d611e42c3485246e34a64eb4", + "sha256": "f0e93ecf57e8deb595c013ff1a9133223ff5aa3552e18c610cf93302b7dd0b86", + "ids": [ + "python_bind-all-interfaces_rule-general-bindall-interfaces" + ], + "cwes": [ + "CWE-1327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-general-bindall-interfaces.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-cipher-modes.yml", + "blob_sha1": "f34f74b689cab925a68fb0962647034fe5cefe71", + "sha256": "579be41611e34d423691c9341ea6e9b5471d35ac934b09d119646ad2bb04b779", + "ids": [ + "python_crypto_rule-cipher-modes" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-cipher-modes.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-cipher-blowfish.yml", + "blob_sha1": "0bddb0fe1bfe104ca94f68a54d28681ed157b0fd", + "sha256": "98432fc326baaeedf04ce105093d30593777e9c50db378bbfb56de606096f349", + "ids": [ + "python_crypto_rule-crypto-cipher-blowfish" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-cipher-blowfish.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-cipher-des.yml", + "blob_sha1": "fe42b44263217363ec934ae5d42e8f8707b7c073", + "sha256": "c4a72951ba44060b5957566aed08086242a7755d256fa791df87b8ffc861b138", + "ids": [ + "python_crypto_rule-crypto-cipher-des" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-cipher-des.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-cipher-rc2.yml", + "blob_sha1": "c7929b3141dfb7c19f058cfa27133ae122add8e0", + "sha256": "4c6b7354a6b6990c37406ff457054b4a1513c654ad0c9a97a7ccca765238ee46", + "ids": [ + "python_crypto_rule-crypto-cipher-rc2" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-cipher-rc2.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-cipher-rc4.yml", + "blob_sha1": "1ca78198b0217a2dc9ae7bf9bb56bb49a3f05620", + "sha256": "1a152e8a7f9fa44f6639df7e199023906983402f434732ae1b9200377aee92dc", + "ids": [ + "python_crypto_rule-crypto-cipher-rc4" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-cipher-rc4.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-cipher-xor.yml", + "blob_sha1": "0b146e8068391148bbd4556f20a91dfeaae63cfc", + "sha256": "a94a1eb095d2c8b4ac1ed9e0c7e118c0fb5a4412edd02fa72e6a57ad6ea0cda6", + "ids": [ + "python_crypto_rule-crypto-cipher-xor" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-cipher-xor.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-encrypt-dsa-rsa.yml", + "blob_sha1": "f180f44cb725c64ce50e8354b1237d255063992e", + "sha256": "9fadf035dac3736e3de1d6d1463cda3659fb7a4281c3cfc19ac3146034f0f517", + "ids": [ + "python_crypto_rule-crypto-encrypt-dsa-rsa" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-encrypt-dsa-rsa.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-encrypt-ec.yml", + "blob_sha1": "28f3de64de0489879e185124a5b6e1466e6a72ed", + "sha256": "c2e7a1c70a6730c575af68b8c0d0a2de803a5f291303f7147f9aa4f62a9c2f6f", + "ids": [ + "python_crypto_rule-crypto-encrypt-ec" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-encrypt-ec.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-hash-md5.yml", + "blob_sha1": "c21873755ddc049c7f0c06da77d87721e7add02e", + "sha256": "d2df42348852a44fcc61bb02a39de831c0940bad9e0069809404cc412f3d7e8a", + "ids": [ + "python_crypto_rule-crypto-hash-md5" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-hash-md5.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-hash-sha1.yml", + "blob_sha1": "2270e611b009a180cc0e45223f3831e6752b1af8", + "sha256": "576a2466dab13cb4cf3d7ab1a034b83c31b12ddf4f077baf25a856ddeb07555b", + "ids": [ + "python_crypto_rule-crypto-hash-sha1" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-hash-sha1.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-arc4.yml", + "blob_sha1": "e93a94c4b5c9c0e7adc32be9993de4e9237ccae0", + "sha256": "86a2fcd4d721ba5050168e6d00b653b8ff8eb14c9847b91dc58db9dec5b120a4", + "ids": [ + "python_crypto_rule-crypto-hazmat-cipher-arc4" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-hazmat-cipher-arc4.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-blowfish.yml", + "blob_sha1": "e85205018bff42db4853c5ebfe61483f2dfb7022", + "sha256": "6664d107499416988b11b624e85fa3158191d0cec5bae71754df6405d62d90e1", + "ids": [ + "python_crypto_rule-crypto-hazmat-cipher-blowfish" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-hazmat-cipher-blowfish.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-idea.yml", + "blob_sha1": "dc07e183cf5e37b0c0370695db946da3b7ae909b", + "sha256": "37e222953c38028e03a1d0c8f595dc87cf694a95a38c8b802aa7acf2af1da657", + "ids": [ + "python_crypto_rule-crypto-hazmat-cipher-idea" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-hazmat-cipher-idea.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-hazmat-hash-md5.yml", + "blob_sha1": "8427a103fc48ed457d9941d04cc8821045fb660b", + "sha256": "d65341d672ab0679e48d063fd026418ea45fb6644b8a3f2f004be18d2d61e9c6", + "ids": [ + "python_crypto_rule-crypto.hazmat-hash-md5" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-hazmat-hash-md5.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-crypto-hazmat-hash-sha1.yml", + "blob_sha1": "7a5a83c9b723d5cfe6036ed49406b01fb0022b61", + "sha256": "2e49a98758c396b7936efc6f6faed848a00f93220319878ed2af5a320d294c34", + "ids": [ + "python_crypto_rule-crypto.hazmat-hash-sha1" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-crypto-hazmat-hash-sha1.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-hash-md2.yml", + "blob_sha1": "2f969f1cae20027dd2651005fcde4149019c02be", + "sha256": "ce8e79cb18041ef60d38012ba0002525d4db8036e90acd944584f0ef9436f24f", + "ids": [ + "python_crypto_rule-hash-md2" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-hash-md2.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-hash-md4.yml", + "blob_sha1": "fc48267c4182b0f8fcfb439ee322a50d1df58502", + "sha256": "a1c01385a5604c3ec52ffb52a1782cc3d1803f42d8bb92e2ac8b1b9375774822", + "ids": [ + "python_crypto_rule-hash-md4" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-hash-md4.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-hash-md5.yml", + "blob_sha1": "9374e341f4ea5460795fccbd61706a7cba1cbdd3", + "sha256": "a1ea675f50c0404e7dcc1c94630a18cfae6251afdcdc17a23922309beb15d09c", + "ids": [ + "python_crypto_rule-hash-md5" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-hash-md5.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-hash-sha1.yml", + "blob_sha1": "e078474e960660248820a169e1ce313284e9907e", + "sha256": "31cbaf048f10d1937c434ff461c962b24336213c1455c3f20cdb0498b3a968da", + "ids": [ + "python_crypto_rule-hash-sha1" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-hash-sha1.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-hashlib-new-insecure-functions.yml", + "blob_sha1": "2c772d23a4df45a96413d340085e1c50a9b66a96", + "sha256": "894ff7b3a25230c15572c24ab7872afcf357ac29965482cf1a36d7ac21ecc43c", + "ids": [ + "python_crypto_rule-hashlib-new-insecure-functions" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-hashlib-new-insecure-functions.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/crypto/rule-import-pycrypto.yml", + "blob_sha1": "2d473352d7954c5b91608aa6f375907384e0e59d", + "sha256": "b78ad33eb3f0c485638d92e028cf42cf12466e7978fd1f49eda3c5dc2786c919", + "ids": [ + "python_crypto_rule-import-pycrypto" + ], + "cwes": [ + "CWE-1104" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-import-pycrypto.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/deserialization/rule-cpickle.yml", + "blob_sha1": "1d93799fd27a61647fc63728e26fbdce8da86bc3", + "sha256": "6f481a1ad60a0b17d23747518da2e884e4642e9c505f17b50eeb4c6db9aadaf8", + "ids": [ + "python_deserialization_rule-cpickle" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-cpickle.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/deserialization/rule-dill.yml", + "blob_sha1": "374b480c80a66a053732aa3f050bd5ce2d4fc85d", + "sha256": "33c5f3b59b33ba8eb1b8f68b6e404d821d5c1c9eda5f3c919784ab4e2ec87fef", + "ids": [ + "python_deserialization_rule-dill" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-dill.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/deserialization/rule-marshal.yml", + "blob_sha1": "f64fba6885be67caf3bcc3601f1b30f3ddd6bb5c", + "sha256": "0a028a60ea0453bafe18c0eb865b9a72a200a5b13aa7d2dd49efb13870ca0dfb", + "ids": [ + "python_deserialization_rule-marshal" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-marshal.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/deserialization/rule-pickle.yml", + "blob_sha1": "11eddb3ef7cdd12d81f17e91ce2909fa43112361", + "sha256": "604febbd00d5d0aec6f12b515330aa99cb39af30e237afa64415fcc70c8a2a8d", + "ids": [ + "python_deserialization_rule-pickle" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-pickle.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/deserialization/rule-shelve.yml", + "blob_sha1": "a6f39ea9ed2d7b8bc2a6416bbfe7de8ea301e113", + "sha256": "c7e2ac0f2571009e65d9596adeffed22ab962270774a046a0cdd5afd9857d1bb", + "ids": [ + "python_deserialization_rule-shelve" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-shelve.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/deserialization/rule-yaml-load.yml", + "blob_sha1": "1733c11f52c1d714c2793a5853ac30199b33d6e0", + "sha256": "f356f893e9b58b637209f0079bc3f60ff4d84b4b3c46858333682d2682e50572", + "ids": [ + "python_deserialization_rule-yaml-load" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-yaml-load.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/django/rule-django-extra-used.yml", + "blob_sha1": "4adbb2c290ee0d29d23c52801b7a149b6aea3835", + "sha256": "37dcc477c25cc1cd092581ec2efc629ca221f4a11675f161e563d276bc3c8576", + "ids": [ + "python_django_rule-django-extra-used" + ], + "cwes": [ + "CWE-89" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-django-extra-used.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/escaping/rule-jinja2-autoescape-false.yml", + "blob_sha1": "a0a7c6a4fdb33359e4702e63f1e33d50df59740a", + "sha256": "4a6273aa13150b0e0a7810e601ccac7fbf24e4b26add58a638424f0789112826", + "ids": [ + "python_escaping_rule-jinja2-autoescape-false" + ], + "cwes": [ + "CWE-116" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-jinja2-autoescape-false.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/escaping/rule-use-of-mako-templates.yml", + "blob_sha1": "826a1f6c05781a7a6858a3c871030b650aab0fb4", + "sha256": "00a0ebcb5cf8a8b3d02fb21c7d9c3c06ef16f6e272430a9b1c4bb82600326e94", + "ids": [ + "python_escaping_rule-use-of-mako-templates" + ], + "cwes": [ + "CWE-79" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-use-of-mako-templates.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/eval/rule-eval.yml", + "blob_sha1": "359cce5bb9ec0c69a39680a9b217f532171c1674", + "sha256": "f0ef96a45a2d432e7cd364883653c35a96acdca162180c7a45c30b9551e9e87d", + "ids": [ + "python_eval_rule-eval" + ], + "cwes": [ + "CWE-95" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-eval.py states no licence", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/exec/rule-exec-used.yml", + "blob_sha1": "79e4e003d8080bac1f61f1ee95a8301583865d62", + "sha256": "00cc80d176a1a801801fb8412894c150902b9e3c29466cd574df2f5d846859a2", + "ids": [ + "python_exec_rule-exec-used" + ], + "cwes": [ + "CWE-95" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-exec-used.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/exec/rule-linux-command-wildcard-injection.yml", + "blob_sha1": "aa774d89ce7d7419d8970148fc6b1c47eecbc215", + "sha256": "c78b58400f82dfb4b16f6d84549d0a587fc7ee00323159fdbee87e7991deb045", + "ids": [ + "python_exec_rule-linux-command-wildcard-injection" + ], + "cwes": [ + "CWE-155" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-linux-command-wildcard-injection.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/exec/rule-os-path.yml", + "blob_sha1": "0d4545edaa19ed9c7d8d34c881e8f5c9d345d1ff", + "sha256": "e02a31e93847850dbcd0d02f5f8aed8fd86a004c62c62729b4bcd9d5c8fd9346", + "ids": [ + "python_exec_rule-os-path" + ], + "cwes": [ + "CWE-78" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-os-path.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/exec/rule-os-popen2.yml", + "blob_sha1": "d0ba0c45ef76d05e6b651202636159b03e3bf8f0", + "sha256": "2dc4551679a2636582dd8c518c3632929e480629ed23fe27352df7383de9218e", + "ids": [ + "python_exec_rule-os-popen2" + ], + "cwes": [ + "CWE-78" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-os-popen2.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/exec/rule-start-process-with-no-shell.yml", + "blob_sha1": "f84f48e628165044875f79fc47569db6c00cccfd", + "sha256": "0538e647b62caee1348d18633b17aaa20a8fc0d2d03ec5113ad450db5e5b34e0", + "ids": [ + "python_exec_rule-start-process-with-no-shell" + ], + "cwes": [ + "CWE-78" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-start-process-with-no-shell.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/exec/rule-subprocess-call.yml", + "blob_sha1": "f80bea8caf3fc94835a92c00fd019e41d1863a51", + "sha256": "874386052dc293105a222211f15e28c3cab47592000682aa6706401033098f5f", + "ids": [ + "python_exec_rule-subprocess-call" + ], + "cwes": [ + "CWE-78" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-subprocess-call.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/exec/rule-subprocess-popen-shell-true.yml", + "blob_sha1": "d4dc537d03443be413e235ad6514bfec4a684aab", + "sha256": "697dc9cf925703ca91cba0231bfb07c44d9a61e8ec159e93bd2263ec6cae251b", + "ids": [ + "python_exec_rule-subprocess-popen-shell-true" + ], + "cwes": [ + "CWE-78" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-subprocess-popen-shell-true.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/exec/rule-subprocess-shell-TRUE.yml", + "blob_sha1": "2c7ec045e54e88dfcfd0d6c3f5ca206845b33f1f", + "sha256": "ed5b254f615cff63a2d390d7c21b3b3e54e14ed5a0994b840d77e5e12fea1046", + "ids": [ + "python_exec_rule-subprocess-shell-TRUE" + ], + "cwes": [ + "CWE-78" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-subprocess-shell-TRUE.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/file_permissions/rule-general-bad-permission.yml", + "blob_sha1": "dca0a2f26b6956709d84cd2b4237565048f82eee", + "sha256": "2b34fbb865d85916558a8388a9bb5bbe0874293b47e0e7f4d6b0d7e56a156437", + "ids": [ + "python_file-permissions_rule-general-bad-permission" + ], + "cwes": [ + "CWE-732" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-general-bad-permission.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/files/rule-tarfile-unsafe-members.yml", + "blob_sha1": "be9b8efb4c0b0d05b5acf6403aa4109b19802c51", + "sha256": "01d488fa3f0b7de5643e5bf5289e3201c9078ed3334f7f874ad0121609e86531", + "ids": [ + "python_files_rule-tarfile-unsafe-members" + ], + "cwes": [ + "CWE-22" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-tarfile-unsafe-members.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/flask/rule-app-debug.yml", + "blob_sha1": "8463dad6bd65a2bbc7bfc8bddb72ed7e6be98991", + "sha256": "6dfa6301da8474b8f57c319bdc4f114decd8ffc08aea393ce51749a9cd42b0cf", + "ids": [ + "python_flask_rule-app-debug" + ], + "cwes": [ + "CWE-489" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-app-debug.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/ftp/rule-ftplib.yml", + "blob_sha1": "a05d0382099ca33ff04350d1e5c6da61b00fe042", + "sha256": "da0a9b91bf9396837a0edf87c63af697322c22148981107a25814df58e0bf3d8", + "ids": [ + "python_ftp_rule-ftplib" + ], + "cwes": [ + "CWE-319" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-ftplib.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/log/rule-logging-config-insecure-listen.yml", + "blob_sha1": "066ebac4b1f12f885b222dc2ce029cdd2ff7597e", + "sha256": "581ff4351a7333e103c735f47f8f1a8f9c6e94981eddc0b789e5e44cd10c0994", + "ids": [ + "python_log_rule-logging-config-insecure-listen" + ], + "cwes": [ + "CWE-94" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-logging-config-insecure-listen.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/random/rule-random.yml", + "blob_sha1": "d4d72ad7a8c39c2b91f6375889f4d8f275f6ba2d", + "sha256": "06610ca04152d6eb2627c169b833df95acfa660e6cc00fd605b6f786847e1a6e", + "ids": [ + "python_random_rule-random" + ], + "cwes": [ + "CWE-338" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-random.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/requests/rule-request-without-timeout.yml", + "blob_sha1": "6c633e3e7ec2f10dcce1ce25d25fad3ecc058e71", + "sha256": "fd3940d67788d0695a019c6e4d4678ebce74428a65919dfefea445ee4a840ef7", + "ids": [ + "python_requests_rule-request-without-timeout" + ], + "cwes": [ + "CWE-770" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-request-without-timeout.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/snmp/rule-insecure-snmp-version.yml", + "blob_sha1": "e43d3d4218556938bf3060af6a0133e9793d76e8", + "sha256": "0030cb025c25a44f291983628ff3459ecf174d6bc693a3e634afaf4170044034", + "ids": [ + "python_snmp_rule-insecure-snmp-version" + ], + "cwes": [ + "CWE-319" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-insecure-snmp-version.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/snmp/rule-snmp-weak-cryptography.yml", + "blob_sha1": "0e8806c679480a12266f8098043135d5000bf8bc", + "sha256": "31c1f1375f4515d8410204387fd07e67d49435faa862f86701aa3f842ea8fc83", + "ids": [ + "python_snmp_rule-snmp-weak-cryptography" + ], + "cwes": [ + "CWE-319" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-snmp-weak-cryptography.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/sql/rule-hardcoded-sql-expression.yml", + "blob_sha1": "a964b3b2c68db4411eb481120005f27f03ef6fab", + "sha256": "0b3efb24d1e631ba2704bf2af3edef7a173990f12f46b107b9128b9ec0ce2323", + "ids": [ + "python_sql_rule-hardcoded-sql-expression" + ], + "cwes": [ + "CWE-89" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-hardcoded-sql-expression.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/ssh/rule-ssh-nohost-key-verification.yml", + "blob_sha1": "6dabedfd113f6d25296f6d21447f9f60737ac809", + "sha256": "eeb3ea435e58b03e1dc7e1dadbf904a6533d8cddd18900c5b126a48686142b7b", + "ids": [ + "python_ssh_rule-ssh-nohost-key-verification" + ], + "cwes": [ + "CWE-322" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-ssh-nohost-key-verification.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/ssl/rule-req-no-certvalid.yml", + "blob_sha1": "ac30a75912362d693cbb162aa666e9cf5fe23741", + "sha256": "cf653caeafd1a007a964d2083be04c6b530ac8aee25de567ea34c42c7c6a94ff", + "ids": [ + "python_ssl_rule-req-no-certvalid" + ], + "cwes": [ + "CWE-295" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-req-no-certvalid.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/ssl/rule-ssl-no-version.yml", + "blob_sha1": "9965f8ae0e6dbaf7838c3d2035082e617f787a06", + "sha256": "202672f5b1b6668a1991b1945b48a73f359343c4ee8f1ca3a12cb595f1c004c7", + "ids": [ + "python_ssl_rule-ssl-no-version" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-ssl-no-version.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/ssl/rule-ssl-with-bad-version.yml", + "blob_sha1": "34dbe9c8fddd8eb3be8ed897d06d4dd39b2080f5", + "sha256": "63fb951999b114edf2a9d1a0d32955833adaed93b298f6d6b0dffa726d497917", + "ids": [ + "python_ssl_rule-ssl-with-bad-version" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-ssl-with-bad-version.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/ssl/rule-unverified-context.yml", + "blob_sha1": "c7fc63f6c38f8a9cee95dea29eb6b87ee85f2635", + "sha256": "b895b4fa102fc0362652c0c6b59a52468c61a38fc9bf6ca5249267968eeb7bc4", + "ids": [ + "python_ssl_rule-unverified-context" + ], + "cwes": [ + "CWE-295" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-unverified-context.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/telnet/rule-import-telnib.yml", + "blob_sha1": "7cb11fb11c254121a0f6955eb2fed564ce787415", + "sha256": "1ecb890e46adfb29bb1580e7f1f7f471e85137c32ad6dda913a940e644351228", + "ids": [ + "python_telnet_rule-import-telnib" + ], + "cwes": [ + "CWE-319" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-import-telnib.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/tmpdir/rule-hardcodedtmp.yml", + "blob_sha1": "0a3e6a7d399d7ddfb277cf6e489ad9804421aec2", + "sha256": "bc7fdab58f75607050580350026e62e31f394e8ac3936475d0ac46009d68884c", + "ids": [ + "python_tmpdir_rule-hardcodedtmp" + ], + "cwes": [ + "CWE-377" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-hardcodedtmp.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/tmpdir/rule-mktemp-q.yml", + "blob_sha1": "12171feff5cc7550ccd16a926a70fd66d4156e83", + "sha256": "fd2b4caafbdfa0a867faf7ea8ed75d563eaa9a02f26844e851fb398e93185f90", + "ids": [ + "python_tmpdir_rule-mktemp-q" + ], + "cwes": [ + "CWE-377" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-mktemp-q.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/urlopen/rule-urllib-urlopen.yml", + "blob_sha1": "b7ba0208df24dfb346c6d437b268486721cec5ce", + "sha256": "f76f741b217c7dd7823596785a4180dc5e7ab2d68c8d21d3970bb4ab6c4ba2f4", + "ids": [ + "python_urlopen_rule-urllib-urlopen" + ], + "cwes": [ + "CWE-918" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-urllib-urlopen.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/xml/rule-celement.yml", + "blob_sha1": "a49efd941890967f07d12d76cdfee0503d60aab1", + "sha256": "b006aa121ecfd7a068fe48853fba6d3c01173906ec7f9b1880ea9c7aad59dd99", + "ids": [ + "python_xml_rule-celement" + ], + "cwes": [ + "CWE-611" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-celement.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/xml/rule-element.yml", + "blob_sha1": "0ceca8fd778286c11754a22a87d70b8c40922116", + "sha256": "52f546c49625a51bc7dfd99e259adc28edfda8f840a264abbe0bf567fb73b4a5", + "ids": [ + "python_xml_rule-element" + ], + "cwes": [ + "CWE-611" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-element.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/xml/rule-etree.yml", + "blob_sha1": "13a9f930d074147516820ca393a1b7bf540e6687", + "sha256": "081a39e7afb105af4abc826dcddf904965ab969c4f41ac7bdc175a0ffd7af2dd", + "ids": [ + "python_xml_rule-etree" + ], + "cwes": [ + "CWE-611" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-etree.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/xml/rule-expatbuilder.yml", + "blob_sha1": "1999d2b946b95bd89920054f20a220c6886f0790", + "sha256": "90f209120fd1b7d2920c533cc88af2e2b98be7b0ecc67408eab12d17039bf437", + "ids": [ + "python_xml_rule-expatbuilder" + ], + "cwes": [ + "CWE-611" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-expatbuilder.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/xml/rule-expatreader.yml", + "blob_sha1": "0704220cd733f809a42241a7274abb056efc8d7a", + "sha256": "06ca936c5fdb0cf79983e198fcfa1af35a2fab5e34da12b921744f91ab54a761", + "ids": [ + "python_xml_rule-expatreader" + ], + "cwes": [ + "CWE-611" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-expatreader.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/xml/rule-minidom.yml", + "blob_sha1": "f9aadc5dd6f8516053f417934f9be363aae0e6aa", + "sha256": "c386a666a6b96c06f85ef3eb213d2b5774e09114621bef5e9e4ebc0fc13e0900", + "ids": [ + "python_xml_rule-minidom" + ], + "cwes": [ + "CWE-611" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-minidom.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/xml/rule-pulldom.yml", + "blob_sha1": "bdfa3dacaf1e62bb75c81c4aa5db879d018c20e2", + "sha256": "932d143eff3e8a95d6be0b0f0acb025c281b2b464019d150e83a978154942c0f", + "ids": [ + "python_xml_rule-pulldom" + ], + "cwes": [ + "CWE-611" + ], + "languages": [ + "python" + ], + "licence": "Apache-2.0", + "licence_evidence": "derived from bandit: its GitLab companion rule-pulldom.py: License: Apache 2.0 (c) PyCQA; the rule's own header says MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/python/xml/rule-sax.yml", + "blob_sha1": "009fb29cf933b161ac870c0519f8b7640c4243f5", + "sha256": "d1b194d9f5037b89bdd2b8034ac47b5a884da855b1577b6c33e6f578a8b8dd8a", + "ids": [ + "python_xml_rule-sax" + ], + "cwes": [ + "CWE-611" + ], + "languages": [ + "python" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-sax.py: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/cookie/rule-RequestParamToCookie.yml", + "blob_sha1": "70f5a5aed7b9ba5edbd30208fcd47ae1b6c14d9b", + "sha256": "93791c1bf99ad774bc2d039822ca148d7da648745cd49a8d33b61d09b9261951", + "ids": [ + "scala_cookie_rule-RequestParamToCookie" + ], + "cwes": [ + "CWE-113" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-RequestParamToCookie.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/cookie/rule-RequestParamToHeader.yml", + "blob_sha1": "2b20048684581e15c81164aeb7f3bd46c76161e7", + "sha256": "b243a695c808a2591fa26ff71e6199f8e4508cbd212373e7dccc65c87a82f6e3", + "ids": [ + "scala_cookie_rule-RequestParamToHeader" + ], + "cwes": [ + "CWE-113" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-RequestParamToHeader.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/cors/rule-PermissiveCORSInjection.yml", + "blob_sha1": "786d2576ff4f4c0210a70bb779704eec3bcd5576", + "sha256": "e91e3dbbd95b100fed4be732af4ec4c89a2c94340055db145f1bc2c61c759844", + "ids": [ + "scala_cors_rule-PermissiveCORSInjection" + ], + "cwes": [ + "CWE-942" + ], + "languages": [ + "java" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-PermissiveCORSInjection.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/crypto/rule-CipherDESInsecure.yml", + "blob_sha1": "dce61a45de81b348514494f320a1d32b86df02a2", + "sha256": "81ee9cc8ae73efeb44fcde6fcc2a2a8c42a61cdd1f0897406eddae561e1395d9", + "ids": [ + "scala_crypto_rule-CipherDESInsecure" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherDESInsecure.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/crypto/rule-CipherDESedeInsecure.yml", + "blob_sha1": "e31dcb858b044bd12ba8a41cc704653bb65a02ea", + "sha256": "21019c52e4d616237d1ae24a12a9b132e995266374bb6aaedc90a9efb9078d7e", + "ids": [ + "scala_crypto_rule-CipherDESedeInsecure" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherDESedeInsecure.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/crypto/rule-CipherECBMode.yml", + "blob_sha1": "f1ccbc8d3c47f41ac9856947aefa89bff1ed3984", + "sha256": "f0b36009bdc4c544d805c254fb53c1cc030a3882937496a47dbe9c53ec853b64", + "ids": [ + "scala_crypto_rule-CipherECBMode" + ], + "cwes": [ + "CWE-326" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherECBMode.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/crypto/rule-CipherIntegrity.yml", + "blob_sha1": "278c234280385dadb6f8b84eff45acf7a33f6ec7", + "sha256": "935002e37c7184169e825ef53090863172c0e620d22b91b36a6c5cab0f98e6f5", + "ids": [ + "scala_crypto_rule-CipherIntegrity" + ], + "cwes": [ + "CWE-353" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherIntegrity.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/crypto/rule-CipherPaddingOracle.yml", + "blob_sha1": "e6223d557475b829e4644a5a5541148bb9a34012", + "sha256": "9cf48594c3433f78753acdc484e9c7af666c137d452adbe8a3f390823eb3252e", + "ids": [ + "scala_crypto_rule-CipherPaddingOracle" + ], + "cwes": [ + "CWE-327" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CipherPaddingOracle.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/inject/rule-CustomInjectionSQLString.yml", + "blob_sha1": "af83591d7f5ca95a54d3d76c5c3b2a6f659430a3", + "sha256": "891c402a3d882b8b7a56a33214a4afdc582e98bfef672147391d397ff0abae3a", + "ids": [ + "scala_inject_rule-CustomInjectionSQLString" + ], + "cwes": [ + "CWE-89" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-CustomInjectionSQLString.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/inject/rule-PathTraversalIn.yml", + "blob_sha1": "ac69982fd1cc5e526b3593d9a18c9d2067ab256a", + "sha256": "e2563829f01cf096252fbf812b7d0bfde491ec12c1ebb7258052d21da1fd18dd", + "ids": [ + "scala_inject_rule-PathTraversalIn" + ], + "cwes": [ + "CWE-22" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-PathTraversalIn.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/inject/rule-PathTraversalOut.yml", + "blob_sha1": "05f4946d173576cabc49c93b4ac31bbf6fca3227", + "sha256": "b0c9dcbd519d6453011ee8d3ea08b35a427d75a9ca0e1afc7a383e81ca20f956", + "ids": [ + "scala_inject_rule-PathTraversalOut" + ], + "cwes": [ + "CWE-22" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-PathTraversalOut.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/inject/rule-SpotbugsPathTraversalAbsolute.yml", + "blob_sha1": "656a2885506e8ccdc673d1720bc7c4cc3476c28b", + "sha256": "023f229cbcbab36a948bb68a1fa459aef3024a42455900f2346043639b3ffe3f", + "ids": [ + "scala_inject_rule-SpotbugsPathTraversalAbsolute" + ], + "cwes": [ + "CWE-22" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-SpotbugsPathTraversalAbsolute.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/inject/rule-SpotbugsPathTraversalRelative.yml", + "blob_sha1": "d3715cc42843fc9d2267f0790b4cbc9168abf4e0", + "sha256": "47c4515d3751940352d83c7eeffb0c02cca5458bf50de906203aca00393cb721", + "ids": [ + "scala_inject_rule-SpotbugsPathTraversalRelative" + ], + "cwes": [ + "CWE-22" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-SpotbugsPathTraversalRelative.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/ldap/rule-EntryPoisoning.yml", + "blob_sha1": "7d7b6d93e795302125d362783c393b7acae28f1a", + "sha256": "4caf84d355fdfe771e7c8ce898e48b1d6451148ffcc5f6d8721fda7429d09004", + "ids": [ + "scala_ldap_rule-EntryPoisoning" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-EntryPoisoning.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/password/rule-ConstantDBPassword.yml", + "blob_sha1": "222d5b82e72d81a5cbf6d15ccc839eef640a6111", + "sha256": "50399d405dc8afca564bb8ec34e05d9990bf8669754cf4428af3778b4787831c", + "ids": [ + "scala_password_rule-ConstantDBPassword" + ], + "cwes": [ + "CWE-259" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-ConstantDBPassword.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/password/rule-EmptyDBPassword.yml", + "blob_sha1": "49fff0106b266daac108c02eb5a7fcd2d3b01eb6", + "sha256": "679b970e0e661b20f79c688c93478d10e21e853ce98e3e5f3f954b46964f77eb", + "ids": [ + "scala_password_rule-EmptyDBPassword" + ], + "cwes": [ + "CWE-259" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-EmptyDBPassword.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/password/rule-HardcodePassword.yml", + "blob_sha1": "3487865ee12db94b254cc414fa8ecf3cfb4d227c", + "sha256": "6b590965adcfbb28569b59e7df6ce2129cecd33f9ead43ac4b77d559f2a26dc1", + "ids": [ + "scala_password_rule-HardcodePassword" + ], + "cwes": [ + "CWE-259" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-HardcodePassword.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/xml/rule-XmlDecoder.yml", + "blob_sha1": "334db2357c1d4901c643e18a155d050dd220f35e", + "sha256": "e7ba1aff74e64f7b2c10709e4f4b6ab9f6cb950075f5d31ea9b3d1a1f295d634", + "ids": [ + "scala_xml_rule-XmlDecoder" + ], + "cwes": [ + "CWE-502" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-XmlDecoder.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "path": "gitlab-sast-rules/scala/xss/rule-XSSServlet.yml", + "blob_sha1": "db9693763a35f5bc0bf5445faebd0df2a90747dc", + "sha256": "c25f5a21dabe50dd5cff2d8314194583abac7e515c41d5ea30c3e8dc8b03e453", + "ids": [ + "scala_xss_rule-XSSServlet" + ], + "cwes": [ + "CWE-79" + ], + "languages": [ + "scala" + ], + "licence": "MIT", + "licence_evidence": "# License: MIT (c) GitLab Inc.; its GitLab companion rule-XSSServlet.scala: License: MIT (c) GitLab Inc.", + "licence_file": "gitlab-sast-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/command-injection.yaml", + "blob_sha1": "92183f2e2188568e3cbe2dd26da473372481ee0e", + "sha256": "9853fbecd89048df3c6e941b3f494d9bfd33f6d3cdfc3312bd1c89e0e89ec5ae", + "ids": [ + "raptor-command-injection" + ], + "cwes": [ + "CWE-78", + "CWE-88", + "CWE-676" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/double-free.yaml", + "blob_sha1": "6cefafdab1d2a7360a85220dd4c3559ece533723", + "sha256": "7f97efbc6bd0c73e55aa762341323218ef80ce05e13f9c767ccc6ba7c7a9f56e", + "ids": [ + "raptor-double-free" + ], + "cwes": [ + "CWE-415" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/format-string-bugs.yaml", + "blob_sha1": "350270ea73ef2b528493be86d786c9cce66b1467", + "sha256": "d6127079635340bd65c9ea2b076401be1b365d552076d740df2eb65989593f5c", + "ids": [ + "raptor-format-string-bugs" + ], + "cwes": [ + "CWE-134" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/incorrect-order-setuid-setgid.yaml", + "blob_sha1": "28a1f8d248b3e99483b078751a3c75cbc3100e0f", + "sha256": "4ab184cebdf06464d3b4195c25d010ddb5a87d87b5a4eaed3a8f9dd12f356eb3", + "ids": [ + "raptor-incorrect-order-setuid-setgid" + ], + "cwes": [ + "CWE-696" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/incorrect-unsigned-comparison.yaml", + "blob_sha1": "549c7e8e3a64ac0119e84b0db1b0e4bc2a1449ce", + "sha256": "131e9eeb059717d38a5be0e0dfa44fc8fde1cfcca1cd998515ce11586ca8b352", + "ids": [ + "raptor-incorrect-unsigned-comparison" + ], + "cwes": [ + "CWE-697" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/incorrect-use-of-free.yaml", + "blob_sha1": "a87f0a4ef03e8b1efbefafe4f5f8b55df799f884", + "sha256": "36f45f59725115bc3a945955d427d1484306b0bc13393ec2d64d3589ad93c860", + "ids": [ + "raptor-incorrect-use-of-free" + ], + "cwes": [ + "CWE-590" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/incorrect-use-of-memset.yaml", + "blob_sha1": "5467115570ab3a585d15b343e5aaa35922b852c3", + "sha256": "a4d32967836736f8c06933454d16775d272dfa448397495b8cbf768d25b04907", + "ids": [ + "raptor-incorrect-use-of-memset" + ], + "cwes": [ + "CWE-683" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/incorrect-use-of-sizeof.yaml", + "blob_sha1": "1e7b26fd2ff3a9251d523f78a1948b4915aa8861", + "sha256": "cd4a492d354a290ce4dc4f481bc82f212dc44478d0ebffb03375284cff147278", + "ids": [ + "raptor-incorrect-use-of-sizeof" + ], + "cwes": [ + "CWE-467" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/incorrect-use-of-strncat.yaml", + "blob_sha1": "478ddf0dd7026c6434ea6768ef68ca0e8f092078", + "sha256": "7450acff6d03c97e2b1c5e92a316afd86536b7b665478c1acbbf3d6206f69899", + "ids": [ + "raptor-incorrect-use-of-strncat" + ], + "cwes": [ + "CWE-787", + "CWE-193" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-access-stat.yaml", + "blob_sha1": "092204a69c6d0df03a64dc651cf19604f95dc830", + "sha256": "d807b74d184e7283e1fdf3193566465a0349918322b2712981432b4c42d9e874", + "ids": [ + "raptor-insecure-api-access-stat" + ], + "cwes": [ + "CWE-367" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-alloca.yaml", + "blob_sha1": "c994acb4a7dd384f32dcc57cf85429bea391d0fe", + "sha256": "9bd9c4dfb3990851bc6938ed646e7f53bdbe765690b8288a1ed6950d3317c9cc", + "ids": [ + "raptor-insecure-api-alloca" + ], + "cwes": [ + "CWE-676", + "CWE-1325" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-ato.yaml", + "blob_sha1": "a7f9e645522f2cfc9f25d8efb4f3f6e084a4952a", + "sha256": "24b73fd7d77bb35ddc9cf71b7dc1450ec8df9444c7d46227841ba30dd91e20df", + "ids": [ + "raptor-insecure-api-ato" + ], + "cwes": [ + "CWE-839" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-gets.yaml", + "blob_sha1": "4f67b55b954aecacfa2e94b19ddfc1aea85886e2", + "sha256": "184afb46808532ae024322322b0bc0171859ef833b79e8ee7ea47a84820d067f", + "ids": [ + "raptor-insecure-api-gets" + ], + "cwes": [ + "CWE-242", + "CWE-120" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-mktemp-tmpnam-tempnam.yaml", + "blob_sha1": "682f26f0fa7439c70c5f6c546a46d24965908022", + "sha256": "60e371556add3bf80b7ac29d2de6eae7f3981c3245c596091005fddc14e954d6", + "ids": [ + "raptor-insecure-api-mktemp-tmpnam-tempnam" + ], + "cwes": [ + "CWE-377", + "CWE-367" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-rand-srand.yaml", + "blob_sha1": "42c3b9a0a71a47d84d0e5f73e17451cb25af524f", + "sha256": "7267d8e5291bc848d1c6baa4da2387d31900f692ccd9df037dd86a1fa93d201f", + "ids": [ + "raptor-insecure-api-rand-srand" + ], + "cwes": [ + "CWE-338", + "CWE-330" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-scanf.yaml", + "blob_sha1": "622777a4759758ac1539a066fb9bf0864bb683a1", + "sha256": "9150c50d7bf403fcbdc4a403c235e43264282628ffa3feb8cdbe91a8cf417814", + "ids": [ + "raptor-insecure-api-scanf" + ], + "cwes": [ + "CWE-676", + "CWE-120", + "CWE-787" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-signal.yaml", + "blob_sha1": "7029f954fe26b94407a1fe3e6b690fbf31113d97", + "sha256": "9d18af572463b2b46bc34ab3041eb311905918292555ca0de8a72beb07fd42cd", + "ids": [ + "raptor-insecure-api-signal" + ], + "cwes": [ + "CWE-364", + "CWE-479", + "CWE-828" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-sprintf-vsprintf.yaml", + "blob_sha1": "a250134090372b9ae3556794b05c2fc8aa40a5d7", + "sha256": "41da105efae2db54897649a199a0b38cbcc6d2077064a7b83b6ca7332d33c875", + "ids": [ + "raptor-insecure-api-sprintf-vsprintf" + ], + "cwes": [ + "CWE-676", + "CWE-120", + "CWE-787" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/insecure-api-strcpy-strcat.yaml", + "blob_sha1": "bbe2119cdb26b9483b29aa3d4c16f4929adc315a", + "sha256": "87585575b9e92174efec337a5a7cbf4c17f176b9ad9a3988224caf8a3f420ed6", + "ids": [ + "raptor-insecure-api-strcpy-strcat" + ], + "cwes": [ + "CWE-676", + "CWE-120", + "CWE-787" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/integer-truncation.yaml", + "blob_sha1": "4987332556cda5ad319014393c9f93ea4981d880", + "sha256": "907821bce0d8882822678465093cd70add09db9232db729f9589c09d15172b6f", + "ids": [ + "raptor-integer-truncation" + ], + "cwes": [ + "CWE-197", + "CWE-681" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/integer-wraparound.yaml", + "blob_sha1": "c2f477b556b7ca43eaf410ca6a294665d350ea5b", + "sha256": "27d4948843de5cccfafe020f16d0904c9a6b05a4d830138b5fe23c7c926fc5c7", + "ids": [ + "raptor-integer-wraparound" + ], + "cwes": [ + "CWE-190", + "CWE-128", + "CWE-131" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/memory-address-exposure.yaml", + "blob_sha1": "37cef3491146e0b9737d46b76988a2bb4665beb0", + "sha256": "269e000d559b001d77f8a03defe2aff1f5e7e518e50623947d46a2ba98dd78fb", + "ids": [ + "raptor-memory-address-exposure" + ], + "cwes": [ + "CWE-209", + "CWE-497" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/mismatched-memory-management-cpp.yaml", + "blob_sha1": "e9123ed43f2a58757ca0b81eae536aa6f5cd34aa", + "sha256": "13be850b69ecc989aa5e611a76590e79806e8cb8e726399ac853034a489400fd", + "ids": [ + "raptor-mismatched-memory-management-cpp" + ], + "cwes": [ + "CWE-762" + ], + "languages": [ + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/mismatched-memory-management.yaml", + "blob_sha1": "a20305220299f07aea0011f52b681a75bd20ac4a", + "sha256": "55331d496f71e8e6eae47c36312a4c09b214fa5092d189cc3772c671c009eaf3", + "ids": [ + "raptor-mismatched-memory-management" + ], + "cwes": [ + "CWE-762" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/off-by-one.yaml", + "blob_sha1": "d32d7d833064cb410452c4650236f0971d738d94", + "sha256": "03123e96d70bbc5e8c93683ee54385e22e4573f252e4cfc03343d60e7df0f1e2", + "ids": [ + "raptor-off-by-one" + ], + "cwes": [ + "CWE-193", + "CWE-787" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/overlapping-source-destination.yaml", + "blob_sha1": "ed218d2a1bcb8be28d590c2d980df664e765f195", + "sha256": "d2545f62ac3566fceeb73056263c80b6089fbcbe7eac0bc811fc7e0da6a9beb8", + "ids": [ + "raptor-overlapping-source-destination" + ], + "cwes": [ + "CWE-1260" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/putenv-stack-var.yaml", + "blob_sha1": "7f90369ea8e649890ee94cf89594118c8fa970df", + "sha256": "921496b89f6a1ea8eed1a5dd49a2a27c5f12eb3e7ddbc51e211ae51a9b25d2d3", + "ids": [ + "raptor-putenv-stack-var" + ], + "cwes": [ + "CWE-686", + "CWE-562" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/regex-dos.yaml", + "blob_sha1": "764eb7cf23eb25b91992c34f73c148328415af97", + "sha256": "6db97a2eade6cf5f3843f26e1deb6d1aed00017820329dd1c2330b119a14845a", + "ids": [ + "raptor-regex-dos" + ], + "cwes": [ + "CWE-1333" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/ret-stack-address.yaml", + "blob_sha1": "627ec7f5f0c1e41c51fdf5446ca905fb8402e477", + "sha256": "c7278ecd49da34c42b11e082c30fe6b4fd0c5e5254f7c8c8e572a4a9fe1b5ddc", + "ids": [ + "raptor-ret-stack-address" + ], + "cwes": [ + "CWE-562" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/unchecked-ret-malloc.yaml", + "blob_sha1": "d75ec2b1a09f34ffcd6ec16ece32bf1c8d628d4b", + "sha256": "1303d90463c816b244081edc7e0647942fe0e1b9f25cf6599ab7bf13d23f3606", + "ids": [ + "raptor-unchecked-ret-malloc" + ], + "cwes": [ + "CWE-252", + "CWE-690" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/unchecked-ret-scanf.yaml", + "blob_sha1": "d4d6cca3094305b011ba8564be36b4cc7e5b1d21", + "sha256": "a60b1f8d6fd38b1d77f889408bd23d5f2dd4bd7ac116885b2a2f9a6d8b8eb8ac", + "ids": [ + "raptor-unchecked-ret-scanf" + ], + "cwes": [ + "CWE-252" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/unchecked-ret-setuid-seteuid.yaml", + "blob_sha1": "da21f83a45935e0dd7bd742adf567c1acc746058", + "sha256": "cdba3b17c797115bd416fc22e92a152331017fd6d9c590e0fd0948079e60bd7f", + "ids": [ + "raptor-unchecked-ret-setuid-seteuid" + ], + "cwes": [ + "CWE-252" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/unsafe-ret-snprintf-vsnprintf.yaml", + "blob_sha1": "4a5c51d01bbea8c3033d797bc81341cfd88e68e7", + "sha256": "729726facc17bed1b92a2c795fbf7144acae5335a92bf026b37a62d228df1645", + "ids": [ + "raptor-unsafe-ret-snprintf-vsnprintf" + ], + "cwes": [ + "CWE-131" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/unsafe-ret-strlcpy-strlcat.yaml", + "blob_sha1": "269178c2051c07a7339fbff863b7a7905e7c10bd", + "sha256": "0e4d1141a5e619e663d4419ff9a66ac4e7df53869262de0570c70079fbf4b21f", + "ids": [ + "raptor-unsafe-ret-strlcpy-strlcat" + ], + "cwes": [ + "CWE-131" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/unsafe-strlen.yaml", + "blob_sha1": "0f2d59c92d5a60ddaddc9a670617963ab1bf19e2", + "sha256": "b6b91d8663d1e5a8d872cd001092a56a90fafd1a6f5d9d859e6153313e490449", + "ids": [ + "raptor-unsafe-strlen" + ], + "cwes": [ + "CWE-190" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/unterminated-string-strncpy.yaml", + "blob_sha1": "cc61eca2f6ccebfb0c89b912d9bb6a6b2a9c605d", + "sha256": "15eeecc12664c983d4b7f02e7c4be2e4bdb6bc7cb4722939df7dd6874f41003f", + "ids": [ + "raptor-unterminated-string-strncpy" + ], + "cwes": [ + "CWE-170", + "CWE-126" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/use-after-free.yaml", + "blob_sha1": "594b494f01a7db17f034c3d809b9ba8fd95263ce", + "sha256": "c9c3d0eb87947adcf0313de1b3257e48c2b8bc59c332c990bcb4ad97f96add2b", + "ids": [ + "raptor-use-after-free" + ], + "cwes": [ + "CWE-416" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/use-of-source-size-in-copy.yaml", + "blob_sha1": "938939df865571fee9429be9341a80401d4f9ad4", + "sha256": "bdfdaa7e03356ab5dd34cd9eb28bafdc43e62b501500dccf1869a61debefa4fa", + "ids": [ + "raptor-use-of-source-size-in-copy" + ], + "cwes": [ + "CWE-806" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + }, + { + "corpus": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "path": "0xdea-semgrep-rules/rules/c/write-into-stack-buffer.yaml", + "blob_sha1": "5be6610551c1dec19e7dd1a0074cdf9ac379bd55", + "sha256": "8316f1d6cec93e9a6d9c963bf5684ab145dd5e2748afdd8b6220bf6b4884a58f", + "ids": [ + "raptor-write-into-stack-buffer" + ], + "cwes": [ + "CWE-121" + ], + "languages": [ + "c", + "cpp" + ], + "licence": "MIT", + "licence_evidence": "the corpus LICENSE body: MIT License, Copyright (c) 2022 raptor; the rule file has no licence header of its own", + "licence_file": "0xdea-semgrep-rules/LICENSE" + } + ], + "excluded_by_licence": [ + { + "path": "gitlab-sast-rules/csharp/cookies/rule-CookieWithoutHttpOnlyFlag.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-CookieWithoutHttpOnlyFlag.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/cookies/rule-CookieWithoutSSLFlag.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-CookieWithoutSSLFlag.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/crypto/rule-CertificateValidationDisabled.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-CertificateValidationDisabled.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/crypto/rule-WeakCipherAlgorithm.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-WeakCipherAlgorithm.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/crypto/rule-WeakHashingFunction.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-WeakHashingFunction.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/crypto/rule-WeakRNG.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-WeakRNG.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/csrf/rule-Csrf.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-Csrf.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/deserialization/rule-InsecureDeserialization.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-InsecureDeserialization.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/endpoint/rule-UnvalidatedRedirect.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-UnvalidatedRedirect.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/injection/rule-CommandInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-CommandInjection.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/injection/rule-LdapInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-LdapInjection.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/injection/rule-SQLInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-SQLInjection.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/injection/rule-XPathInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-XPathInjection.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/injection/rule-XmlDocumentXXEInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-XmlDocumentXXEInjection.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/injection/rule-XmlReaderXXEInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-XmlReaderXXEInjection.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/other/rule-UnsafeXSLTSettingUsed.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-UnsafeXSLTSettingUsed.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/password/rule-PasswordComplexity.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-PasswordComplexity.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/path/rule-PathTraversal.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-PathTraversal.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/validation/rule-InputValidation.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-InputValidation.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/xss/rule-HtmlElementXss.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-HtmlElementXss.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/csharp/xss/rule-ScriptXss.yml", + "licence": "LGPL-3.0", + "evidence": "derived from security-code-scan: its GitLab companion rule-ScriptXss.cs: License: LGPL-3.0 License (c) security-code-scan; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/cookie/rule-CookieInsecure.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CookieInsecure.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/cookie/rule-HttpResponseSplitting.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-HttpResponseSplitting.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-BlowfishKeySize.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-BlowfishKeySize.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-CustomMessageDigest.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CustomMessageDigest.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-HazelcastSymmetricEncryption.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-HazelcastSymmetricEncryption.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-InsufficientKeySizeRsa.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-InsufficientKeySizeRsa.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-NullCipher.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-NullCipher.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-RsaNoPadding.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-RsaNoPadding.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-WeakMessageDigest.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-WeakMessageDigest.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-WeakTLSProtocolDefaultHttpClient.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-WeakTLSProtocolDefaultHttpClient.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/crypto/rule-WeakTLSProtocolSSLContext.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-WeakTLSProtocolSSLContext.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/endpoint/rule-HostnameVerifier.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-HostnameVerifier.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/endpoint/rule-X509TrustManager.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-X509TrustManager.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/file/rule-FileUploadFileName.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FileUploadFileName.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/file/rule-FilenameUtils.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FilenameUtils.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/inject/rule-CommandInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CommandInjection.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/inject/rule-FileDisclosureRequestDispatcher.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FileDisclosureRequestDispatcher.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/inject/rule-FileDisclosureSpringFramework.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FileDisclosureSpringFramework.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/inject/rule-HttpParameterPollution.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-HttpParameterPollution.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/inject/rule-LDAPInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-LDAPInjection.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/inject/rule-OgnlInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-OgnlInjection.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/inject/rule-SpotbugsPathTraversalAbsolute.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SpotbugsPathTraversalAbsolute.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/ldap/rule-AnonymousLDAP.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-AnonymousLDAP.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/password/rule-HardcodePassword.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-HardcodePassword.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/perm/rule-DangerousPermissions.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-DangerousPermissions.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/perm/rule-OverlyPermissiveFilePermissionInline.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-OverlyPermissiveFilePermissionInline.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/script/rule-ScriptInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ScriptInjection.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/script/rule-SpringSpelExpressionParser.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SpringSpelExpressionParser.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/smtp/rule-InsecureSmtp.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-InsecureSmtp.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/smtp/rule-SmtpClient.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SmtpClient.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/ssrf/rule-SSRF.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SSRF.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/strings/rule-BadHexConversion.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-BadHexConversion.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/strings/rule-FormatStringManipulation.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FormatStringManipulation.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/strings/rule-ModifyAfterValidation.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ModifyAfterValidation.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/strings/rule-NormalizeAfterValidation.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-NormalizeAfterValidation.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/templateinjection/rule-TemplateInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-TemplateInjection.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/unsafe/rule-ExternalConfigControl.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ExternalConfigControl.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/xml/rule-SAMLIgnoreComments.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SAMLIgnoreComments.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/xml/rule-XsltTransform.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XsltTransform.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/xss/rule-WicketXSS.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-WicketXSS.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/xss/rule-XSSReqParamToServletWriter.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XSSReqParamToServletWriter.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/java/xxe/rule-XMLRdr.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XMLRdr.java: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/cookie/rule-CookieHTTPOnly.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CookieHTTPOnly.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/cookie/rule-CookieInsecure.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CookieInsecure.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/cookie/rule-CookiePersistent.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CookiePersistent.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/cookie/rule-CookieUsage.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CookieUsage.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/cookie/rule-HttpResponseSplitting.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-HttpResponseSplitting.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/cookie/rule-TrustBoundaryViolation.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-TrustBoundaryViolation.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/cors/rule-PermissiveCORS.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-PermissiveCORS.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-BlowfishKeySize.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-BlowfishKeySize.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-CustomMessageDigest.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CustomMessageDigest.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-DefaultHTTPClient.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-DefaultHTTPClient.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-HazelcastSymmetricEncryption.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-HazelcastSymmetricEncryption.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-InsufficientKeySizeRsa.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-InsufficientKeySizeRsa.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-NullCipher.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-NullCipher.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-RsaNoPadding.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-RsaNoPadding.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-WeakMessageDigest.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-WeakMessageDigest.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/crypto/rule-WeakTLSProtocol.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-WeakTLSProtocol.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/endpoint/rule-JaxRsEndpoint.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-JaxRsEndpoint.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/endpoint/rule-JaxWsEndpoint.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-JaxWsEndpoint.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/endpoint/rule-UnencryptedSocket.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-UnencryptedSocket.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/endpoint/rule-UnvalidatedRedirect.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-UnvalidatedRedirect.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/endpoint/rule-WeakHostNameVerification.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-WeakHostNameVerification.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/file/rule-FileUploadFileName.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FileUploadFileName.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/file/rule-FilenameUtils.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FilenameUtils.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/form/rule-FormValidate.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FormValidate.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-AWSQueryInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-AWSQueryInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-BeanPropertyInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-BeanPropertyInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-CLRFInjectionLogs.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CLRFInjectionLogs.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-CommandInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CommandInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-CustomInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-CustomInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-ELInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ELInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-FileDisclosure.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FileDisclosure.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-HttpParameterPollution.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-HttpParameterPollution.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-LDAPInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-LDAPInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-OgnlInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-OgnlInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/inject/rule-SqlInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SqlInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/ldap/rule-AnonymousLDAP.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-AnonymousLDAP.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/perm/rule-DangerousPermissions.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-DangerousPermissions.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/perm/rule-OverlyPermissiveFilePermissionInline.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-OverlyPermissiveFilePermissionInline.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/perm/rule-OverlyPermissiveFilePermissionObj.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-OverlyPermissiveFilePermissionObj.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/script/rule-ScriptInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ScriptInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/script/rule-SpelView.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SpelView.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/smtp/rule-InsecureSmtp.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-InsecureSmtp.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/smtp/rule-SmtpClient.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SmtpClient.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/ssrf/rule-PlaySSRF.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-PlaySSRF.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/ssrf/rule-SSRF.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SSRF.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/strings/rule-BadHexConversion.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-BadHexConversion.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/strings/rule-FormatStringManipulation.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-FormatStringManipulation.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/strings/rule-ImproperUnicode.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ImproperUnicode.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/strings/rule-ModifyAfterValidation.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ModifyAfterValidation.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/strings/rule-NormalizeAfterValidation.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-NormalizeAfterValidation.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/templateinjection/rule-TemplateInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-TemplateInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/unsafe/rule-ExternalConfigControl.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ExternalConfigControl.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/unsafe/rule-InformationExposure.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-InformationExposure.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/unsafe/rule-SensitiveDataExposure.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SensitiveDataExposure.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xml/rule-ApacheXmlRpc.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-ApacheXmlRpc.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xml/rule-SAMLIgnoreComments.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SAMLIgnoreComments.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xml/rule-XsltTransform.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XsltTransform.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xpathi/rule-XpathInjection.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XpathInjection.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xss/rule-MVCApi.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-MVCApi.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xss/rule-RequestWrapper.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-RequestWrapper.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xss/rule-WicketXSS.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-WicketXSS.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xss/rule-XSSReqParamToServletWriter.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XSSReqParamToServletWriter.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xxe/rule-Document.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-Document.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xxe/rule-SaxParserXXE.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-SaxParserXXE.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xxe/rule-Trans.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-Trans.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xxe/rule-XMLRdr.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XMLRdr.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xxe/rule-XMLStreamRdr.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XMLStreamRdr.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + }, + { + "path": "gitlab-sast-rules/scala/xxe/rule-XPathXXE.yml", + "licence": "LGPL-3.0", + "evidence": "derived from find-sec-bugs: its GitLab companion rule-XPathXXE.scala: License: LGPL-3.0 License (c) find-sec-bugs; the rule's own header says MIT (c) GitLab Inc." + } + ] +} diff --git a/data/rules/gitlab-sast-rules/LICENSE b/data/rules/gitlab-sast-rules/LICENSE new file mode 100644 index 0000000..e703dbc --- /dev/null +++ b/data/rules/gitlab-sast-rules/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2011-present GitLab Inc. + +Portions of this software are licensed as follows: + +* All content residing under the "doc/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license". +* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE". +* All content that resides under the "jh/" directory of this repository, if that directory exists, is licensed under the license defined in "jh/LICENSE". +* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license. +* All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/data/rules/gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt b/data/rules/gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/data/rules/gitlab-sast-rules/LICENSE.bandit-Apache-2.0.txt @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/data/rules/gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt b/data/rules/gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt new file mode 100644 index 0000000..1756c78 --- /dev/null +++ b/data/rules/gitlab-sast-rules/LICENSE.gosec-Apache-2.0.txt @@ -0,0 +1,154 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable copyright license to +reproduce, prepare Derivative Works of, publicly display, publicly perform, +sublicense, and distribute the Work and such Derivative Works in Source or +Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, +each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those patent claims +licensable by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) with the Work +to which such Contribution(s) was submitted. If You institute patent litigation +against any entity (including a cross-claim or counterclaim in a lawsuit) +alleging that the Work or a Contribution incorporated within the Work +constitutes direct or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate as of the date +such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and in +Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and You must cause any modified files to carry prominent notices +stating that You changed the files; and You must retain, in the Source form of +any Derivative Works that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, excluding those notices +that do not pertain to any part of the Derivative Works; and If the Work +includes a "NOTICE" text file as part of its distribution, then any Derivative +Works that You distribute must include a readable copy of the attribution +notices contained within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one of the following +places: within a NOTICE text file distributed as part of the Derivative Works; +within the Source form or documentation, if provided along with the Derivative +Works; or, within a display generated by the Derivative Works, if and wherever +such third-party notices normally appear. The contents of the NOTICE file are +for informational purposes only and do not modify the License. You may add Your +own attribution notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided that such +additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. 5. Submission of Contributions. +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in +writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied, including, without limitation, any warranties +or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any risks +associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in +tort (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to in +writing, shall any Contributor be liable to You for damages, including any +direct, indirect, special, incidental, or consequential damages of any character +arising as a result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, work stoppage, +computer failure or malfunction, or any and all other commercial damages or +losses), even if such Contributor has been advised of the possibility of such +damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or +Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such +obligations, You may act only on Your own behalf and on Your sole +responsibility, not on behalf of any other Contributor, and only if You agree to +indemnify, defend, and hold each Contributor harmless for any liability incurred +by, or claims asserted against, such Contributor by reason of your accepting any +such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/data/rules/gitlab-sast-rules/csharp/crypto/rule-WeakCipherMode.yml b/data/rules/gitlab-sast-rules/csharp/crypto/rule-WeakCipherMode.yml new file mode 100644 index 0000000..a5ce3fe --- /dev/null +++ b/data/rules/gitlab-sast-rules/csharp/crypto/rule-WeakCipherMode.yml @@ -0,0 +1,88 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "csharp_crypto_rule-WeakCipherMode" + languages: + - "csharp" + patterns: + - pattern-inside: | + using System.Security.Cryptography; + ... + - metavariable-regex: + metavariable: "$CIPHER" + regex: "^(ECB|CBC|OFB|CFB|CTS)$" + - pattern: "CipherMode.$CIPHER" + message: | + Cryptographic algorithms provide many different modes of operation, only some of which provide + message integrity. Without message integrity it could be possible for an adversary to attempt + to tamper with the ciphertext which could lead to compromising the encryption key. Newer + algorithms + apply message integrity to validate ciphertext has not been tampered with. + + Instead of using an algorithm that requires configuring a `CipherMode`, an algorithm + that has built-in message integrity should be used. If using .NET Framework greater + than version 6.0 consider using `ChaCha20Poly1305` or `AES-256-GCM`. + + For older applications, `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are re-used. + + Example using `ChaCha20Poly1305`: + ``` + // Generate a random key + byte[] key = new byte[32]; + RandomNumberGenerator.Fill(key); + + ChaCha20Poly1305 encryptor = new ChaCha20Poly1305(key); + + // Note nonce values _must_ be regenerated every time they are used. + var nonce = new byte[12]; + RandomNumberGenerator.Fill(nonce); + + byte[] plainText = System.Text.Encoding.UTF8.GetBytes("Secret text to encrypt"); + byte[] cipherText = new byte[plainText.Length]; + var authTag = new byte[16]; + + encryptor.Encrypt(nonce, plainText, cipherText, authTag); + byte[] output = new byte[cipherText.Length]; + encryptor.Decrypt(nonce, cipherText, authTag, output); + Console.WriteLine("Output: {0}", System.Text.Encoding.UTF8.GetString(output)); + ``` + + Example using `AES-256-GCM`: + ``` + var plaintextBytes = Encoding.UTF8.GetBytes("Secret text to encrypt"); + var key = new byte[32]; + RandomNumberGenerator.Fill(key); + + using var aes = new AesGcm(key); + var nonce = new byte[AesGcm.NonceByteSizes.MaxSize]; + RandomNumberGenerator.Fill(nonce); + + var cipherText = new byte[plaintextBytes.Length]; + var tag = new byte[AesGcm.TagByteSizes.MaxSize]; + + aes.Encrypt(nonce, plaintextBytes, cipherText, tag); + + // Decrypt + using (var decrypt = new AesGcm(key)) + { + var decryptedBytes = new byte[cipherText.Length]; + + decrypt.Decrypt(nonce, cipherText, tag, decryptedBytes); + + Console.WriteLine("Decrypted: {0}", Encoding.UTF8.GetString(decryptedBytes)); + } + ``` + severity: "WARNING" + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + category: "security" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + cwe: "CWE-327" + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-des.yml b/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-des.yml new file mode 100644 index 0000000..59e9c42 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-des.yml @@ -0,0 +1,115 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/blocklist.go +# yamllint enable +--- +rules: +- id: "go_blocklist_rule-blocklist-des" + languages: + - "go" + patterns: + - pattern: | + import "crypto/des" + message: | + The DES algorithm has not been recommended for over 15 years and was withdrawn from NIST (FIPS + 46-3) in 2005. It is recommended that an algorithm that provides message integrity be used + instead. Consider using `XChaCha20Poly1305` or `AES-256-GCM`. + + For older applications, `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `XChaCha20Poly1305` + - Smaller nonce value size compared to `XChaCha20Poly1305` + - Catastrophic failure if nonce values are re-used + + Example using + [XChaCha20Poly1305](https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305#NewX): + ``` + key := make([]byte, chacha20poly1305.KeySize) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + log.Fatal(err) + } + + // NewX is a variant that uses longer nonce values for better security + aead, err := chacha20poly1305.NewX(key) + if err != nil { + log.Fatal(err) + } + + var encrypted = []byte{} + var nonce = []byte{} + + // Encryption routine + { + msg := []byte("Some secret message") + nonce = make([]byte, aead.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + log.Fatal("failed to generate nonce") + } + + encrypted = aead.Seal(nil, nonce, msg, nil) + } + + // Decryption routine + { + if len(encrypted) < aead.NonceSize() { + log.Fatal("incorrect ciphertext length") + } + + msg, err := aead.Open(nil, nonce, encrypted, nil) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Decrypted: %s\n", msg) + } + ``` + + Example using [AES-256-GCM](https://pkg.go.dev/crypto/cipher#NewGCM): + ``` + // 32 byte keys will configure AES-256 + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + log.Fatal(err) + } + + blockCipher, err := aes.NewCipher(key) + if err != nil { + log.Fatal(err) + } + + aead, err := cipher.NewGCM(blockCipher) + if err != nil { + log.Fatal(err) + } + + var encrypted = []byte{} + var nonce = []byte{} + // Encryption routine + { + msg := []byte("Some secret message") + // note that the key must be rotated every 2^32 random nonces used otherwise + // cipher text could be repeated + nonce = make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + log.Fatal(err) + } + encrypted = aead.Seal(nil, nonce, msg, nil) + } + + // Decryption routine + { + msg, err := aead.Open(nil, nonce, encrypted, nil) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Decrypted: %s\n", msg) + } + ``` + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-md5.yml b/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-md5.yml new file mode 100644 index 0000000..d1e9d5f --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-md5.yml @@ -0,0 +1,113 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/blocklist.go +# yamllint enable +--- +rules: +- id: "go_blocklist_rule-blocklist-md5" + languages: + - "go" + patterns: + - pattern: | + import "crypto/md5" + message: | + The MD5 message-digest algorithm has been cryptographically broken and is unsuitable for + further use. The MD5 hash algorithm has been found to be vulnerable to producing collisions. + This means that two different values, when hashed, can lead to the same hash value. It is + recommended that the SHA-3 or BLAKE2 family of algorithms be used for non-password based + cryptographic hashes instead. For password based cryptographic hashes, consider using the + bcrypt or Argon2id family of cryptographic hashes. + + Hashing values using [BLAKE2](https://pkg.go.dev/golang.org/x/crypto/blake2b): + ``` + fileContents := []byte("some file contents to create hash for") + blake2bHasher, err := blake2b.New512(nil) + if err != nil { + log.Fatal(err) + } + hashedValue := blake2bHasher.Sum(fileContents) + fmt.Printf("%s\n", hex.EncodeToString(hashedValue)) + ``` + + Hashing and securely comparing passwords using + [Argon2id](https://pkg.go.dev/golang.org/x/crypto/argon2#hdr-Argon2id): + ``` + type argonParameters struct { + variant string + version int + memory uint32 + iterations uint32 + parallelism uint8 + saltLength uint32 + keyLength uint32 + } + + func (a argonParameters) StringFormat(salt, derivedKey []byte) string { + encodedSalt := base64.RawStdEncoding.EncodeToString(salt) + encodedKey := base64.RawStdEncoding.EncodeToString(derivedKey) + + return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, + a.memory, + a.iterations, + a.parallelism, + encodedSalt, + encodedKey, + ) + } + + func main() { + // Initialize Argon2id parameters + p := argonParameters{ + memory: 64 * 1024, + iterations: 3, + parallelism: 2, + saltLength: 16, + keyLength: 32, + } + + // Generate random salt (to be stored alongside derived hash key) + salt := make([]byte, p.saltLength) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { + log.Fatal(err) + } + + usersPassword := []byte("User's Very S3cur3P4ss@rd@#$%") + + var derivedKey []byte + // Create key hash derived from user's password + { + derivedKey = argon2.IDKey(usersPassword, salt, p.iterations, p.memory, p.parallelism, + p.keyLength) + // store p.StringFormat(...) result in a data store... + fmt.Printf("%s\n", p.StringFormat(salt, derivedKey)) + } + + // Verify a user's password against key + { + keyToCompare := argon2.IDKey(usersPassword, salt, p.iterations, p.memory, p.parallelism, + p.keyLength) + + // Use subtle.ConstantTimeCompare(..., ...) to ensure no side channel leaks used in timing + attacks + if subtle.ConstantTimeCompare(derivedKey, keyToCompare) == 1 { + fmt.Printf("Passwords match\n") + } else { + fmt.Printf("Passwords do not match\n") + } + } + } + ``` + + For more information on password storage see OWASP's guide: + https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-rc4.yml b/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-rc4.yml new file mode 100644 index 0000000..21ec66d --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-rc4.yml @@ -0,0 +1,115 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/blocklist.go +# yamllint enable +--- +rules: +- id: "go_blocklist_rule-blocklist-rc4" + languages: + - "go" + patterns: + - pattern: | + import "crypto/rc4" + message: | + The RC4 stream-cipher has been cryptographically broken and is unsuitable + for use in production. It is recommended that ChaCha20 or Advanced Encryption + Standard (AES) be used instead. Consider using `XChaCha20Poly1305` or `AES-256-GCM`. + + For older applications, `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `XChaCha20Poly1305` + - Smaller nonce value size compared to `XChaCha20Poly1305` + - Catastrophic failure if nonce values are re-used + + Example using + [XChaCha20Poly1305](https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305#NewX): + ``` + key := make([]byte, chacha20poly1305.KeySize) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + log.Fatal(err) + } + + // NewX is a variant that uses longer nonce values for better security + aead, err := chacha20poly1305.NewX(key) + if err != nil { + log.Fatal(err) + } + + var encrypted = []byte{} + var nonce = []byte{} + + // Encryption routine + { + msg := []byte("Some secret message") + nonce = make([]byte, aead.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + log.Fatal("failed to generate nonce") + } + + encrypted = aead.Seal(nil, nonce, msg, nil) + } + + // Decryption routine + { + if len(encrypted) < aead.NonceSize() { + log.Fatal("incorrect ciphertext length") + } + + msg, err := aead.Open(nil, nonce, encrypted, nil) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Decrypted: %s\n", msg) + } + ``` + + Example using [AES-256-GCM](https://pkg.go.dev/crypto/cipher#NewGCM): + ``` + // 32 byte keys will configure AES-256 + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + log.Fatal(err) + } + + blockCipher, err := aes.NewCipher(key) + if err != nil { + log.Fatal(err) + } + + aead, err := cipher.NewGCM(blockCipher) + if err != nil { + log.Fatal(err) + } + + var encrypted = []byte{} + var nonce = []byte{} + // Encryption routine + { + msg := []byte("Some secret message") + // note that the key must be rotated every 2^32 random nonces used otherwise + // cipher text could be repeated + nonce = make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + log.Fatal(err) + } + encrypted = aead.Seal(nil, nonce, msg, nil) + } + + // Decryption routine + { + msg, err := aead.Open(nil, nonce, encrypted, nil) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Decrypted: %s\n", msg) + } + ``` + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-sha1.yml b/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-sha1.yml new file mode 100644 index 0000000..edb06d3 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/blocklist/rule-blocklist-sha1.yml @@ -0,0 +1,112 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/blocklist.go +# yamllint enable +--- +rules: +- id: "go_blocklist_rule-blocklist-sha1" + languages: + - "go" + patterns: + - pattern: | + import "crypto/sha1" + message: | + The SHA-1 message-digest algorithm has been cryptographically broken and + is unsuitable for further use. It is + recommended that the SHA-3, or BLAKE2 family of algorithms be used for non-password based + cryptographic hashes instead. For password based cryptographic hashes, consider using the + bcrypt or Argon2id family of cryptographic hashes. + + Hashing values using [BLAKE2](https://pkg.go.dev/golang.org/x/crypto/blake2b): + ``` + fileContents := []byte("some file contents to create hash for") + blake2bHasher, err := blake2b.New512(nil) + if err != nil { + log.Fatal(err) + } + hashedValue := blake2bHasher.Sum(fileContents) + fmt.Printf("%s\n", hex.EncodeToString(hashedValue)) + ``` + + Hashing and securely comparing passwords using + [Argon2id](https://pkg.go.dev/golang.org/x/crypto/argon2#hdr-Argon2id): + ``` + type argonParameters struct { + variant string + version int + memory uint32 + iterations uint32 + parallelism uint8 + saltLength uint32 + keyLength uint32 + } + + func (a argonParameters) StringFormat(salt, derivedKey []byte) string { + encodedSalt := base64.RawStdEncoding.EncodeToString(salt) + encodedKey := base64.RawStdEncoding.EncodeToString(derivedKey) + + return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, + a.memory, + a.iterations, + a.parallelism, + encodedSalt, + encodedKey, + ) + } + + func main() { + // Initialize Argon2id parameters + p := argonParameters{ + memory: 64 * 1024, + iterations: 3, + parallelism: 2, + saltLength: 16, + keyLength: 32, + } + + // Generate random salt (to be stored alongside derived hash key) + salt := make([]byte, p.saltLength) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { + log.Fatal(err) + } + + usersPassword := []byte("User's Very S3cur3P4ss@rd@#$%") + + var derivedKey []byte + // Create key hash derived from user's password + { + derivedKey = argon2.IDKey(usersPassword, salt, p.iterations, p.memory, p.parallelism, + p.keyLength) + // store p.StringFormat(...) result in a data store... + fmt.Printf("%s\n", p.StringFormat(salt, derivedKey)) + } + + // Verify a user's password against key + { + keyToCompare := argon2.IDKey(usersPassword, salt, p.iterations, p.memory, p.parallelism, + p.keyLength) + + // Use subtle.ConstantTimeCompare(..., ...) to ensure no side channel leaks used in timing + attacks + if subtle.ConstantTimeCompare(derivedKey, keyToCompare) == 1 { + fmt.Printf("Passwords match\n") + } else { + fmt.Printf("Passwords do not match\n") + } + } + } + ``` + + For more information on password storage see OWASP's guide: + https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/crypto/rule-badtlssettings.yml b/data/rules/gitlab-sast-rules/go/crypto/rule-badtlssettings.yml new file mode 100644 index 0000000..80b8936 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/crypto/rule-badtlssettings.yml @@ -0,0 +1,88 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/tls_config.go +# yamllint enable +--- +rules: +- id: "go_crypto_rule-badtlssettings" + languages: + - "go" + patterns: + - pattern-either: + - pattern: | + tls.Config{..., CipherSuites: []$SLICE{..., $CIPHERS, ...}, ...} + - pattern: | + tls.CipherSuite{..., ID: $CIPHERS, ...} + - metavariable-regex: + metavariable: "$CIPHERS" + regex: "((?!tls.TLS_AES_128_GCM_SHA256)|(?!tls.TLS_AES_256_GCM_SHA384)|(?!tls.TLS_CHACHA20_POLY1305_SHA256)| + (?!tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)|(?!tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)| + (?!tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)|(?!tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)| + (?!tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305)|(?!tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)| + (?!tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305)|(?!tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256)| + (?!tls.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)|(?!tls.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384))" + message: | + Usage of a cryptographically insecure cipher suite has been detected. It is recommended that + alternative ciphers be used instead. It is strongly recommended that all TLS connections + use TLS 1.3 as Go will automatically choose the most secure cipher when negotiating the + TLS handshake with client or servers. TLS 1.3 cipher suites are configured to require Perfect + Forward Secrecy (PFS). + PFS is an important property as it will ensure that past encrypted transmissions could not be + decrypted + if the TLS certificate was compromised. + + Example using TLS 1.3 for a Go server: + ``` + cert, err := tls.LoadX509KeyPair("server.crt", "server.key") + if err != nil { + log.Fatal(err) + } + + cfg := &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS13} + srv := &http.Server{ + Addr: ":8999", + TLSConfig: cfg, + ReadTimeout: time.Minute, + WriteTimeout: time.Minute, + } + log.Fatal(srv.ListenAndServeTLS("", "")) + ``` + + If TLS 1.0-1.2 must be used, then the following list of ciphers should be chosen as they + support + Perfect Forward Secrecy (PFS): + + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 + - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 + + + Example `tls.Config` using the recommended cipher suites: + ``` + cfg := &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + }, + } + ``` + + For more information on cipher suites in Go see: https://go.dev/blog/tls-cipher-suites + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + cwe: "CWE-327" + category: "security" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/crypto/rule-insecure-ignore-host-key.yml b/data/rules/gitlab-sast-rules/go/crypto/rule-insecure-ignore-host-key.yml new file mode 100644 index 0000000..fd3d6a7 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/crypto/rule-insecure-ignore-host-key.yml @@ -0,0 +1,52 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/ssh.go +# hash: 2a4064d +# yamllint enable +--- +rules: +- id: "go_crypto_rule-insecure-ignore-host-key" + languages: + - "go" + patterns: + - pattern: "ssh.InsecureIgnoreHostKey(...)" + message: | + The application was found to ignore host keys. Host keys are important as + they provide assurance that the client can prove that the host is trusted. + By ignoring these host keys, it is impossible for the client to validate the + connection is to a trusted host. + + For the `ssh.ClientConfig` `HostKeyCallback` property, consider using the + [knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) package that + parses OpenSSH's `known_hosts` key database. + + Example configuration connecting to a known, trusted host: + ``` + knownHostCallback, err := knownhosts.New("/home/user/.ssh/known_hosts") + if err != nil { + log.Fatal(err) + } + + // Create client config using the knownHost callback function + config := &ssh.ClientConfig{ + ... + HostKeyCallback: knownHostCallback, + } + + // Connect to ssh server + conn, err := ssh.Dial("tcp", "localhost:22", config) + if err != nil { + log.Fatal("unable to connect: ", err) + } + defer conn.Close() + ``` + metadata: + shortDescription: "Key exchange without entity authentication" + cwe: "CWE-322" + owasp: + - "A2:2017-Broken Authentication" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/crypto/rule-tlsversion.yml b/data/rules/gitlab-sast-rules/go/crypto/rule-tlsversion.yml new file mode 100644 index 0000000..95b23d2 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/crypto/rule-tlsversion.yml @@ -0,0 +1,75 @@ +# yamllint disable +# License: License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: + - id: "go_crypto_rule-tlsversion" + languages: + - "go" + pattern-either: + - patterns: + - pattern-either: + - patterns: + - pattern-inside: | + tls.Config{...} + - pattern: | + MinVersion: $VAL + - patterns: + - pattern-inside: | + $VAR = uint16($VAL) + ... + - pattern-inside: | + tls.Config{...} + - pattern: | + MinVersion: $VAR + - metavariable-pattern: + metavariable: $VAL + pattern-either: + - pattern: "tls.VersionTLS11" + - pattern: "tls.VersionTLS10" + - patterns: + - pattern-inside: | + tls.Config{...} + - pattern: | + MaxVersion: $ANYVAL + - pattern-not-inside: | + tls.Config{..., MinVersion: ..., ...} + message: | + TLS versions 1.1 and 1.0 were deprecated by the IETF in June 2018 due to + a number of attacks against the vulnerable versions. Use of a deprecated + TLS version may result in the unauthorized retrieval of sensitive + information. It is strongly recommended that all TLS connections + use TLS 1.3 as Go will automatically choose the most secure cipher when + negotiating the TLS handshake with client or servers. TLS 1.3 cipher suites + are configured to require Perfect Forward Secrecy (PFS). PFS is an important + property as it will ensure that past encrypted transmissions could not be + decrypted if the TLS certificate was compromised. + + Example using TLS 1.3 for a Go server: + ``` + cert, err := tls.LoadX509KeyPair("server.crt", "server.key") + if err != nil { + log.Fatal(err) + } + + cfg := &tls.Config{Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS13} + + srv := &http.Server{ + Addr: ":8999", + TLSConfig: cfg, + ReadTimeout: time.Minute, + WriteTimeout: time.Minute, + } + log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem")) + ``` + metadata: + shortDescription: "Selection of less-secure algorithm during negotiation ('algorithm downgrade')" + cwe: "CWE-757" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/crypto/rule-weakkeystrength.yml b/data/rules/gitlab-sast-rules/go/crypto/rule-weakkeystrength.yml new file mode 100644 index 0000000..4b88f37 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/crypto/rule-weakkeystrength.yml @@ -0,0 +1,48 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/rsa.go +# yamllint enable +--- +rules: +- id: "go_crypto_rule-weakkeystrength" + languages: + - "go" + patterns: + - pattern-either: + - pattern: | + rsa.GenerateKey(..., $ARG) + - metavariable-comparison: + metavariable: "$ARG" + comparison: "$ARG < 2048" + message: | + The application is generating an RSA key that is less than the recommended 2048 bits. + The National Institute of Standards and Technology (NIST) deprecated signing Digital + Certificates that contained RSA Public Keys of 1024 bits in December 2010. While + 1024-bit RSA keys have not been factored yet, advances in compute may make it possible + in the near future. + + To generate an RSA key of 2048 pass the number of bits as the second parameter to + the `rsa.GenerateKey` function: + ``` + import ( + "crypto/rand" + "crypto/rsa" + ) + + func generate() { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Fatal(err) + } + } + ``` + metadata: + shortDescription: "Inadequate encryption strength" + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/crypto/rule-weakrandsource.yml b/data/rules/gitlab-sast-rules/go/crypto/rule-weakrandsource.yml new file mode 100644 index 0000000..1b6e8bb --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/crypto/rule-weakrandsource.yml @@ -0,0 +1,40 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/rand.go +# yamllint enable +--- +rules: +- id: "go_crypto_rule-weakrandsource" + languages: + - "go" + patterns: + - patterns: + - pattern-inside: | + import $IMPORT "math/rand" + ... + - pattern-not-inside: | + import "crypto/rand" + - pattern-either: + - pattern: "$IMPORT.$METHOD(...)" + - pattern: "rand.$METHOD(...)" + - metavariable-regex: + metavariable: "$METHOD" + regex: "(Float32|Float64|Int31|Int31n|Int63|Int63n|NormalFloat64|Uint32|Uint64)" + message: | + Go's `math/rand` is not meant for use in generating random numbers for any cryptographic or + security sensitive context. This includes generating random numbers that could be used in + user specific identifiers or where the random number that is generated is considered to + be secret. + + Replace all imports of `math/rand` with `crypto/rand`. + metadata: + shortDescription: "Use of cryptographically weak Pseudo-Random Number Generator + (PRNG)" + cwe: "CWE-338" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/file_permissions/rule-fileperm.yml b/data/rules/gitlab-sast-rules/go/file_permissions/rule-fileperm.yml new file mode 100644 index 0000000..a6e5bc8 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/file_permissions/rule-fileperm.yml @@ -0,0 +1,51 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/fileperms.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_file-permissions_rule-fileperm" + languages: + - "go" + patterns: + - pattern-either: + - pattern: "os.Chmod(...,$MASK)" + - pattern: "os.OpenFile(...,$MASK)" + - pattern: "os.WriteFile(...,$MASK)" + - metavariable-comparison: + metavariable: "$MASK" + comparison: "int($MASK) > 0o640" + # comparison: "$MASK > 0o640" + # base: 8 + message: | + The application was found setting file permissions to overly permissive values. Consider + using the following values if the application user is the only process to access + the file: + + - 0400 - read only access to the file + - 0200 - write only access to the file + - 0600 - read/write access to the file + + Example creating a file with read/write permissions for the application user: + ``` + f, err := os.OpenFile("file.txt", os.O_CREATE, 0600) + if err != nil { + log.Fatal(err) + } + defer f.Close() + // continue to work with file here + ``` + + For all other values please see: + https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation + metadata: + shortDescription: "Incorrect permission assignment for critical resource" + cwe: "CWE-732" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/file_permissions/rule-mkdir.yml b/data/rules/gitlab-sast-rules/go/file_permissions/rule-mkdir.yml new file mode 100644 index 0000000..87767da --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/file_permissions/rule-mkdir.yml @@ -0,0 +1,47 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/fileperms.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_file-permissions_rule-mkdir" + languages: + - "go" + patterns: + - pattern-either: + - pattern: "os.Mkdir(...,$MASK)" + - pattern: "os.MkdirAll(...,$MASK)" + - metavariable-comparison: + metavariable: "$MASK" + comparison: "$MASK > 0o750" + base: 8 + message: | + The application was found setting directory permissions to overly permissive values. Consider + using the following values if the application user is the only process to access + files in the directory specified: + - 0700 - read/write access to the files in the directory + + Another common value is `0750` which allows the application user read/write access and group + users to read the files contained in the directory. + + Example creating a directory with read/write permissions for only the application user: + ``` + err := os.Mkdir("directory", 0700) + if err != nil { + log.Fatal(err) + } + ``` + + For all other values please see: + https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation + metadata: + shortDescription: "Incorrect permission assignment for critical resource" + cwe: "CWE-732" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/filesystem/rule-decompression-bomb.yml b/data/rules/gitlab-sast-rules/go/filesystem/rule-decompression-bomb.yml new file mode 100644 index 0000000..577e35f --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/filesystem/rule-decompression-bomb.yml @@ -0,0 +1,72 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/decompression-bomb.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_filesystem_rule-decompression-bomb" + languages: + - "go" + mode: "taint" + pattern-sources: + - pattern: "gzip.NewReader(...)" + - pattern: "zlib.NewReader(...)" + - pattern: "bzip2.NewReader(...)" + - pattern: "flate.NewReader(...)" + - pattern: "lzw.NewReader(...)" + - pattern: "tar.NewReader(...)" + - pattern: "zip.NewReader(...)" + - pattern: "zlib.NewReaderDict(...)" + - pattern: "flate.NewReaderDict(...)" + - pattern: "zip.OpenReader(...)" + pattern-sanitizers: + - patterns: + - pattern: "io.LimitReader($TAINTED, ...)" + - focus-metavariable: "$TAINTED" + pattern-sinks: + - patterns: + - pattern: "io.Copy($DST, $TAINTED)" + - focus-metavariable: "$TAINTED" + - patterns: + - pattern: "io.CopyBuffer($DST, $TAINTED, $BUF)" + - focus-metavariable: "$TAINTED" + message: | + Directly decompressing files or buffers may lead to a potential Denial of Service (DoS) + due to a decompression bomb. Decompression bombs are maliciously compressed files + or data that decompresses to extremely large sizes. This can cause the process to run + out of memory, or the disk to fill up. + + To protect against decompression bombs, an + [io.LimitReader(...)](https://pkg.go.dev/io#LimitReader) + should be used to limit how much can be read during the decompression routine. + + Example using `io.LimitReader` to protect against a decompression bomb: + ``` + f, err := os.Open("some.gz") + if err != nil { + log.Fatal(err) + } + + r, err := gzip.NewReader(f) + if err != nil { + log.Fatal(err) + } + + const oneMegabyte = 1024 * 1024 + limitedReader := io.LimitReader(r, oneMegabyte) + + // use limitedReader to stop copying after 1 MB + if _, err := io.Copy(os.Stdout, limitedReader); err != nil { + log.Fatal(err) + } + ``` + metadata: + shortDescription: "Improper handling of highly compressed data" + cwe: "CWE-409" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/filesystem/rule-fileread.yml b/data/rules/gitlab-sast-rules/go/filesystem/rule-fileread.yml new file mode 100644 index 0000000..3164669 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/filesystem/rule-fileread.yml @@ -0,0 +1,126 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/readfile.go +# yamllint enable +--- +rules: +- id: "go_filesystem_rule-fileread" + languages: + - "go" + mode: "taint" + pattern-sources: + - pattern: "os.Getenv(...)" + - pattern: "fmt.Sprintf(...)" + - pattern: "filepath.Join(...)" + - pattern: "path.Join(...)" + - patterns: + - pattern-either: + - pattern: "... + $TAINTED" + - pattern: "... + $TAINTED + ..." + - pattern: "$TAINTED + ..." + - pattern-not: "\"...\" + $TAINTED" + - pattern-not: "\"...\" + $TAINTED + \"...\"" + - pattern-not: "$TAINTED + \"...\"" + - pattern-not: "fmt.Sprintf(\"...\", \"...\")" + - patterns: + - pattern-either: + - pattern: | + ($REQUEST : *http.Request).$SOURCE_METHOD + - pattern: | + ($REQUEST : http.Request).$SOURCE_METHOD + - metavariable-regex: + metavariable: "$SOURCE_METHOD" + regex: "^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$" + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: | + $CLEAN := $PKG.Clean(...) + ... + if !strings.HasPrefix($CLEAN, "...") {...} + - pattern: | + $CLEAN := $PKG.Clean(...) + ... + if strings.HasPrefix($CLEAN, "...") {...} + - pattern: | + $CLEAN := $PKG.Clean(...) + ... + if strings.HasPrefix($CLEAN, "...") == false {...} + - metavariable-regex: + metavariable: "$PKG" + regex: "^((file)?path)$" + pattern-sinks: + - pattern: "os.OpenFile(...)" + - pattern: "os.Open(...)" + - pattern: "os.ReadFile(...)" + - pattern: "ioutil.ReadFile(...)" + message: | + The application dynamically constructs file or path information. If the path + information comes from user input, it could be abused to read sensitive files, + access other users data or aid in exploitation to gain further system access. + + User input should never be used in constructing paths or files for interacting + with the filesystem. This includes filenames supplied by user uploads or downloads. + If possible, consider hashing user input or replacing it with unique values. + Additionally, use `filepath.Base` to only use the filename and not path information. + Always validate the full path prior to opening or writing to any file. + + Example using `filepath.Base`, generating a unique filename without using + user input to construct filepath information: + ``` + type userData struct { + id string + userFilename string + } + + func newUserData(userFilename string) userData { + return userData{ + id: randomFileID(), // random id as the filename + userFilename: userFilename, + } + } + + // randomFileID generates a random id, to be used as a filename + func randomFileID() string { + id := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, id); err != nil { + log.Fatal(err) + } + return hex.EncodeToString(id) + } + + func main() { + + // user input, saved only as a reference + data := newUserData("../../possibly/malicious") + + // restrict all file access to this path + const basePath = "/tmp/" + + // resolve the full path, but only use our random generated id + resolvedPath, err := filepath.Join(basePath, filepath.Base(data.id)) + if err != nil { + log.Fatal(err) + } + + // verify the path is prefixed with our basePath + if !strings.HasPrefix(resolvedPath, basePath) { + log.Fatal("path does not start with basePath") + } + // process / work with file + } + ``` + + For more information on path traversal issues see OWASP: + https://owasp.org/www-community/attacks/Path_Traversal + metadata: + shortDescription: "Improper limitation of a pathname to a restricted directory + ('Path Traversal')" + cwe: "CWE-22" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/filesystem/rule-httprootdir.yml b/data/rules/gitlab-sast-rules/go/filesystem/rule-httprootdir.yml new file mode 100644 index 0000000..8f9917e --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/filesystem/rule-httprootdir.yml @@ -0,0 +1,43 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/directory-traversal.go +# yamllint enable +--- +rules: +- id: "go_filesystem_rule-httprootdir" + languages: + - "go" + patterns: + - pattern-either: + - pattern: | + import $NET "net/http" + ... + $NET.Dir("/") + - pattern: | + import "net/http" + ... + http.Dir("/") + message: | + The application is potentially exposing the entire filesystem by mounting the root + directory `/` to an HTTP handler function. Anyone who is able to access this HTTP + server may be able to access any file that the HTTP server has access to. + + Restrict the `http.Dir` path to only a specific folder instead of the entire + filesystem. + + Example server only allowing directory listing on a public directory: + ``` + const path = "/var/www/html/public" + fs := http.FileServer(http.Dir(path)) + log.Fatal(http.ListenAndServe(":9000", fs)) + ``` + metadata: + shortDescription: "Files or directories accessible to external parties" + cwe: "CWE-552" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/filesystem/rule-poorwritepermissions.yml b/data/rules/gitlab-sast-rules/go/filesystem/rule-poorwritepermissions.yml new file mode 100644 index 0000000..8adc1de --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/filesystem/rule-poorwritepermissions.yml @@ -0,0 +1,46 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/fileperms.go +# yamllint enable +--- +rules: +- id: "go_filesystem_rule-poorwritepermissions" + languages: + - "go" + patterns: + - pattern-either: + - pattern: | + ioutil.WriteFile(..., ..., $ARG) + - metavariable-comparison: + metavariable: "$ARG" + comparison: "$ARG > 0o600" + base: 8 + message: | + The application was found setting file permissions to overly permissive values. Consider + using the following values if the application user is the only process to access + the file: + + - 0400 - read only access to the file + - 0200 - write only access to the file + - 0600 - read/write access to the file + + Example writing file contents with read/write permissions for the application user: + ``` + dat := []byte("sensitive data") + if err := os.WriteFile("file.txt", dat, 0600); err != nil { + log.Fatal(err) + } + ``` + + For all other values please see: + https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation + metadata: + shortDescription: "Incorrect default permissions" + cwe: "CWE-276" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/filesystem/rule-tempfiles.yml b/data/rules/gitlab-sast-rules/go/filesystem/rule-tempfiles.yml new file mode 100644 index 0000000..1170b4b --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/filesystem/rule-tempfiles.yml @@ -0,0 +1,57 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/tempfiles.go +# yamllint enable +--- +rules: +- id: "go_filesystem_rule-tempfiles" + languages: + - "go" + patterns: + - pattern-either: + - pattern: | + os.WriteFile("$ARG", ...) + - pattern: | + ioutil.WriteFile("$ARG", ...) + - pattern: | + os.OpenFile("$ARG", <... os.O_CREATE ...>, ...) + - pattern: | + os.Create("$ARG") + - metavariable-regex: + metavariable: "$ARG" + regex: "(/tmp/.*|/var/tmp/.*)" + message: | + The application was found creating files in shared system temporary directories + (`/tmp` or `/var/tmp`) without using the `os.CreateTemp` function. Depending + on how the application uses this temporary file, an attacker may be able to create + symlinks that point to other files prior to the application creating or writing + to the target file, leading to unintended files being created or overwritten. + + Example using `os.CreateTemp` in an application restricted directory: + ``` + // assumes /opt/appdir/ is chown'd to the running application user + if err := os.MkdirAll("/opt/appdir/restricted", 0700); err != nil { + log.Fatal(err) + } + + // create a temporary file in the restricted directory in the form of temp-952569059.txt + f, err := os.CreateTemp("/opt/appdir/restricted", "temp-*.txt") + if err != nil { + log.Fatal(err) + } + + defer f.Close() + // clean up on exit + defer os.Remove(f.Name()) + // work with file + ``` + metadata: + shortDescription: "Creation of temporary file with insecure permissions" + cwe: "CWE-378" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/filesystem/rule-ziparchive.yml b/data/rules/gitlab-sast-rules/go/filesystem/rule-ziparchive.yml new file mode 100644 index 0000000..57cb070 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/filesystem/rule-ziparchive.yml @@ -0,0 +1,141 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/archive.go +# yamllint enable +--- +rules: +- id: "go_filesystem_rule-ziparchive" + languages: + - "go" + mode: "taint" + pattern-sources: + - pattern: "zip.OpenReader(...)" + - pattern: "tar.OpenReader(...)" + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: | + $CLEAN := $PKG.Clean(...) + ... + if !strings.HasPrefix($CLEAN, "...") {...} + - pattern: | + $CLEAN := $PKG.Clean(...) + ... + if strings.HasPrefix($CLEAN, "...") {...} + - pattern: | + $CLEAN := $PKG.Clean(...) + ... + if strings.HasPrefix($CLEAN, "...") == false {...} + - metavariable-regex: + metavariable: "$PKG" + regex: "^((file)?path)$" + pattern-sinks: + - pattern: "filepath.Join(...)" + message: | + The application may be vulnerable to a path traversal if it extracts untrusted archive files. + This vulnerability is colloquially known as 'Zip Slip'. Archive files may contain folders + which, + when extracted, may write outside of the intended directory. This is exploited by including + path traversal characters such as `../../other/directory` to overwrite or place files in system + or application directories. + + Extra care must be taken when extracting archive files as there are numerous concerns: + + - Limit the size of the zip archive as it may contain "Zip Bombs", files that extract to + extremely + large sizes. + - If possible, generate unique filenames instead of using the archives file names, as it may be + possible for users to overwrite files if the filenames are the same. + - Validate file paths are written with a prefixed, known trusted directory. + - Only process regular files and not symbolic links, as some applications may attempt to + read/follow + the symbolic link, leading to arbitrary file read / write vulnerabilities. + + + Example of securely processing an archive file: + ``` + r, err := zip.OpenReader("trusted.zip") + if err != nil { + log.Fatal(err) + } + + // Ensure archive contains only the expected number of files + const expectedFileCount = 10 + if len(r.File) > expectedFileCount { + log.Fatalf("too many files in archive: %d\n", len(r.File)) + } + + // One approach is to sum up all files before attempting to process + // them. + const totalAllowedSize = 1024 * 1024 * 10 // 10MB + var totalSize uint64 + for _, f := range r.File { + totalSize += f.UncompressedSize64 + } + + if totalSize > totalAllowedSize { + log.Fatalf("archive exceeds total allowed size: %d\n", totalSize) + } + + // configure a max size per file allowed + const maxFileSize = 1024 * 1024 // 1 MB + + // set restricted basePath + const basePath = "/var/restricted/" + + // iterate over the files in the archive + for _, f := range r.File { + + // Ensure uncompressed size does not exceed our allowed file size + if f.UncompressedSize64 > maxFileSize { + log.Printf("skipping file as it exceeds maxFileSize: %s\n", f.Name) + continue + } + + // Ensure file is a regular file and not a symbolic link or has other mode type + // bits set + if !f.Mode().IsRegular() { + log.Printf("skipping non regular file: %s\n", f.Name) + continue + } + + // if possible consider not using the name at all, but generating a random id instead. + // If the filename must be used, extract the base name and not folder path information + name := filepath.Base(f.Name) + + // Join the file name to the basePath. + resolvedPath := filepath.Join(basePath, name) + + // Application must still verify the path is prefixed by the basePath + if !strings.HasPrefix(resolvedPath, basePath) { + log.Fatal("path does not start with basePath") + } + + // process / work with file + } + ``` + + If the application must process directory names as well, use the following code: + ``` + // Join the cleaned name to the basePath, note if 'name' starts with `../../` it + // will still allow for traversal, so you _must_ verify the path prefix below + resolvedPath := filepath.Join(basePath, filepath.Clean(name)) + + // Application must still verify the path is prefixed by the basePath + if !strings.HasPrefix(resolvedPath, basePath) { + log.Fatal("path does not start with basePath") + } + + // process / work with file + ``` + metadata: + shortDescription: "Improper limitation of a pathname to a restricted directory + ('Path Traversal')" + cwe: "CWE-22" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/http/rule-http-serve.yml b/data/rules/gitlab-sast-rules/go/http/rule-http-serve.yml new file mode 100644 index 0000000..9a7424e --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/http/rule-http-serve.yml @@ -0,0 +1,127 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/http_serve.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_http_rule-http-serve" + languages: + - "go" + patterns: + - pattern-inside: | + import "net/http" + ... + - pattern-either: + - pattern: "http.ListenAndServe(...)" + - pattern: "http.ListenAndServeTLS(...)" + - pattern: "http.Serve(...)" + - pattern: "http.ServeTLS(...)" + - patterns: + - pattern-not-inside: | + &http.Server{ + ..., + ReadHeaderTimeout: ..., + ..., + } + - pattern-not-inside: | + &http.Server{ + ..., + ReadTimeout: ..., + ..., + } + - pattern-not-inside: | + $S = &http.Server{ + ..., + } + $S.ReadHeaderTimeout = ... + ... + - pattern-not-inside: | + $S = &http.Server{ + ..., + } + $S.ReadTimeout = ... + ... + - pattern: | + &http.Server{ + ..., + } + message: | + Go's `net/http` serve functions may be vulnerable to resource consumption attacks if timeouts + are not properly configured + prior to starting the HTTP server. An adversary may open up thousands of connections but never + complete sending all data, + or never terminate the connections. This may lead to the server no longer accepting new + connections. + + To protect against this style of resource consumption attack, timeouts should be set in the + `net/http` server prior to calling + the listen or serve functions. What this means is that the default `http.ListenAndServe` and + `http.Serve` functions should not + be used in a production setting as they are unable to have timeouts configured. Instead a + custom `http.Server` object must be + created with the timeouts configured. + + Example setting timeouts on a `net/http` server: + ``` + // All values chosen below are dependent on application logic and + // should be tailored per use-case + srv := &http.Server{ + Addr: "localhost:8000", + // ReadHeaderTimeout is the amount of time allowed to read + // request headers. The connection's read deadline is reset + // after reading the headers and the Handler can decide what + // is considered too slow for the body. If ReadHeaderTimeout + // is zero, the value of ReadTimeout is used. If both are + // zero, there is no timeout. + ReadHeaderTimeout: 15 * time.Second, + + // ReadTimeout is the maximum duration for reading the entire + // request, including the body. A zero or negative value means + // there will be no timeout. + // + // Because ReadTimeout does not let Handlers make per-request + // decisions on each request body's acceptable deadline or + // upload rate, most users will prefer to use + // ReadHeaderTimeout. It is valid to use them both. + ReadTimeout: 15 * time.Second, + + // WriteTimeout is the maximum duration before timing out + // writes of the response. It is reset whenever a new + // request's header is read. Like ReadTimeout, it does not + // let Handlers make decisions on a per-request basis. + // A zero or negative value means there will be no timeout. + WriteTimeout: 10 * time.Second, + + // IdleTimeout is the maximum amount of time to wait for the + // next request when keep-alives are enabled. If IdleTimeout + // is zero, the value of ReadTimeout is used. If both are + // zero, there is no timeout. + IdleTimeout: 30 * time.Second, + } + + // For per request timeouts applications can wrap all `http.HandlerFunc(...)` in + // `http.TimeoutHandler`` and specify a timeout, but note the TimeoutHandler does not + // start ticking until all headers have been read. + + // Listen with our custom server with timeouts configured + if err := srv.ListenAndServe(); err != nil { + log.Fatal(err) + } + ``` + For more information on the `http.Server` timeouts, see: https://pkg.go.dev/net/http#Server + + For information on setting request based timeouts, see: + https://pkg.go.dev/net/http#TimeoutHandler + + For more information on the Slowloris attack see: + https://en.wikipedia.org/wiki/Slowloris_(computer_security) + metadata: + shortDescription: "Allocation of resources without limits or throttling" + cwe: "CWE-770" + owasp: + - "A6:2017-Security Misconfiguration" + - "A05:2021-Security Misconfiguration" + security-severity: "Low" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/injection/rule-ssrf.yml b/data/rules/gitlab-sast-rules/go/injection/rule-ssrf.yml new file mode 100644 index 0000000..414accd --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/injection/rule-ssrf.yml @@ -0,0 +1,176 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/directory-traversal.go +# yamllint enable +rules: +- id: "go_injection_rule-ssrf" + languages: + - "go" + mode: taint + # At the moment we cannot use the function calls both as sources and sinks + # (which would be a valid use case for SSRF); this would require the + # https://semgrep.dev/docs/writing-rules/experiments/taint-labels/ which is + # experimental. + pattern-sources: + - patterns: + - pattern-not-inside: | + import "testing" + ... + - pattern-either: + - pattern: "os.Stdin" + - pattern: "os.Getenv(...)" + - pattern: | + ($REQ: *http.Request).$ANY + - pattern: | + ($REQ: http.Request).$ANY + - patterns: + - pattern: "($REQ : *http.Request)" + - pattern-inside: | + func $FUNC( $W http.ResponseWriter, $R *http.Request, ...) { + ... + } + pattern-sinks: + - pattern: "http.Head(...)" + - pattern: "http.Get(...)" + - pattern: "http.Post(...)" + - pattern: "http.PostForm(...)" + - pattern: "http.NewRequest($METHOD,...)" + - pattern: "http.DefaultClient.Head(...)" + - pattern: "http.DefaultClient.Get(...)" + - pattern: "http.DefaultClient.Post(...)" + - pattern: "http.DefaultClient.PostForm(...)" + - pattern: "http.NewRequestWithContext($CONTEXT, $METHOD, ...)" + - pattern: "ftp.Dial(...)" + - pattern: "ldap.DialURL(...)" + - pattern: "smtp.Dial(...)" + - pattern: "retryablehttp.NewRequest($METHOD, ..., $BODY)" + - patterns: + - pattern-inside: | + $C := retryablehttp.NewClient() + ... + - pattern-either: + - pattern: "$C.Get(...)" + - pattern: "$C.Post(..., $BODYTYPE, $BODY)" + - pattern: "$C.PostForm(..., $VALS)" + - pattern: "$C.Head(...)" + pattern-propagators: + - pattern: $R := $D.NewDecoder($P) + from: $P + to: $R + - pattern: $S.Decode(&$P) + from: $S + to: $P + - pattern: $S.Decode($P) + from: $S + to: $P + - pattern: $S.Unmarshal($B, &$P) + from: $B + to: $P + - pattern: $S.Unmarshal($B, $P) + from: $B + to: $P + message: | + Server-Side-Request-Forgery (SSRF) exploits backend systems that initiate requests to third + parties. + If user input is used in constructing or sending these requests, an attacker could supply + malicious + data to force the request to other systems or modify request data to cause unwanted actions. + + Ensure user input is not used directly in constructing URLs or URIs when initiating requests + to third party + systems from back end systems. Care must also be taken when constructing payloads using user + input. Where + possible restrict to known URIs or payloads. Consider using a server side map where key's are + used to return + URLs such as `https://site/goto?key=1` where `{key: 1, url: 'http://some.url/', key: 2, url: + 'http://...'}`. + + If you must use user supplied input for requesting URLs, it is strongly recommended that the + HTTP client + chosen allows you to customize and block certain IP ranges at the network level. By blocking + RFC 1918 + addresses or other network address ranges, you can limit the severity of a successful SSRF + attack. Care must + also be taken to block certain protocol or address formatting such as IPv6. + + If you can not block address ranges at the client level, you may want to run the HTTP client + as a protected + user, or in a protected network where you can apply IP Table or firewall rules to block access + to dangerous + addresses. Finally, if none of the above protections are available, you could also run a + custom HTTP proxy + and force all requests through it to handle blocking dangerous addresses. + + Example HTTP client that disallows access to loopback and RFC-1918 addresses + ``` + // IsDisallowedIP parses the ip to determine if we should allow the HTTP client to continue + func IsDisallowedIP(hostIP string) bool { + ip := net.ParseIP(hostIP) + return ip.IsMulticast() || ip.IsUnspecified() || ip.IsLoopback() || ip.IsPrivate() + } + + // SafeTransport uses the net.Dial to connect, then if successful check if the resolved + // ip address is disallowed. We do this due to hosts such as localhost.lol being resolvable to + // potentially malicious URLs. We allow connection only for resolution purposes. + func SafeTransport(timeout time.Duration) *http.Transport { + return &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + c, err := net.DialTimeout(network, addr, timeout) + if err != nil { + return nil, err + } + ip, _, _ := net.SplitHostPort(c.RemoteAddr().String()) + if IsDisallowedIP(ip) { + return nil, errors.New("ip address is not allowed") + } + return c, err + }, + DialTLS: func(network, addr string) (net.Conn, error) { + dialer := &net.Dialer{Timeout: timeout} + c, err := tls.DialWithDialer(dialer, network, addr, &tls.Config{}) + if err != nil { + return nil, err + } + + ip, _, _ := net.SplitHostPort(c.RemoteAddr().String()) + if IsDisallowedIP(ip) { + return nil, errors.New("ip address is not allowed") + } + + err = c.Handshake() + if err != nil { + return c, err + } + + return c, c.Handshake() + }, + TLSHandshakeTimeout: timeout, + } + } + + func httpRequest(requestUrl string) { + const clientConnectTimeout = time.Second * 10 + httpClient := &http.Client{ + Transport: SafeTransport(clientConnectTimeout), + } + resp, err := httpClient.Get(requestUrl) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + // work with resp + } + ``` + + For more information on SSRF see OWASP: + https://owasp.org/www-community/attacks/Server_Side_Request_Forgery + metadata: + shortDescription: "Server Side Request Forgery (SSRF)" + cwe: "CWE-918" + owasp: + - "A1:2017-Injection" + - "A10:2021-Server-Side Request Forgery" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/injection/rule-template-injection.yml b/data/rules/gitlab-sast-rules/go/injection/rule-template-injection.yml new file mode 100644 index 0000000..83cbef3 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/injection/rule-template-injection.yml @@ -0,0 +1,90 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/templates.go +# hash: 2a4064d +# yamllint enable +--- +rules: +- id: "go_injection_rule-template-injection" + languages: + - "go" + patterns: + - pattern-either: + - patterns: + - pattern: "template.HTML($IN)" + - pattern-not: "template.HTML(\"...\")" + - patterns: + - pattern: "template.JS($IN)" + - pattern-not: "template.JS(\"...\")" + - patterns: + - pattern: "template.URL($IN)" + - pattern-not: "template.URL(\"...\")" + - patterns: + - pattern: "template.HTMLAttr($IN)" + - pattern-not: "template.HTMLAttr(\"...\")" + message: | + Cross Site Scripting (XSS) is an attack which exploits a web application or system to treat + user input + as markup or script code. It is important to encode the data depending on the specific context + it + is used in. There are at least six context types: + + - Inside HTML tags `
context 1
` + - Inside attributes: `
` + - Inside event attributes `` + - Inside script blocks: `` + - Unsafe element HTML assignment: `element.innerHTML = "context 5"` + - Inside URLs: `link` + + Script blocks alone have multiple ways they need to be encoded. Extra care must be taken if + user input + is ever output inside of script tags. + + User input that is displayed within the application must be encoded, sanitized or validated + to ensure it cannot be treated as HTML or executed as Javascript code. Care must also be + taken + to not mix server-side templating with client-side templating, as the server-side templating + will + not encode things like {{ 7*7 }} which may execute client-side templating features. + + It is _NOT_ advised to encode user input prior to inserting into a data store. The data will + need to be + encoded depending on context of where it is output. It is much safer to force the displaying + system to + handle the encoding and not attempt to guess how it should be encoded. + + Use of the following template types with user input denotes a security risk: + + - [template.HTML](https://pkg.go.dev/html/template#HTML) + - [template.JS](https://pkg.go.dev/html/template#JS) + - [template.URL](https://pkg.go.dev/html/template#URL) + - [template.HTMLAttr](https://pkg.go.dev/html/template#HTMLAttr) + + Either remove these types from the application or hardcode as const strings prior + to conversion: + ``` + testTemplate, err := template.New("testTemplate").Funcs(template.FuncMap{ + "SafeHTML": func() template.HTML { + const safeHTML = "
hardcoded, safe html
" + return template.HTML(safeHTML) + }, + }).Parse(`{{ SafeHTML }}`) + if err != nil { + log.Fatal(err) + } + + if err := testTemplate.Execute(os.Stdout, nil); err != nil { + log.Fatal(err) + } + ``` + metadata: + shortDescription: "Improper neutralization of input during web page generation + ('Cross-site Scripting')" + cwe: "CWE-79" + owasp: + - "A7:2017-Cross-Site Scripting (XSS)" + - "A03:2021-Injection" + - "A05:2025-Injection" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/leak/rule-pprof-endpoint.yml b/data/rules/gitlab-sast-rules/go/leak/rule-pprof-endpoint.yml new file mode 100644 index 0000000..a39f8b4 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/leak/rule-pprof-endpoint.yml @@ -0,0 +1,38 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/pprof.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_leak_rule-pprof-endpoint" + languages: + - "go" + patterns: + - pattern-inside: | + import ( + "net/http/pprof" + ) + ... + - pattern-either: + - pattern: "http.ListenAndServe(...)" + - pattern: "http.ListenAndServeTLS(...)" + - pattern: "http.Serve(...)" + - pattern: "http.ServeTLS(...)" + message: | + Go has a built in profiling service that is enabled by starting an HTTP server with + `net/http/pprof` imported. The `/debug/pprof` endpoint does not require any + authentication and can be accessed by anonymous users. This profiling endpoint + can leak sensitive information and should not be enabled in production. + + To remediate this, remove the `net/http/pprof` import from the file. + metadata: + shortDescription: "Active debug code (pprof enabled)" + cwe: "CWE-489" + owasp: + - "A6:2017-Security Misconfiguration" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + security-severity: "Medium" + category: "security" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/go/memory/rule-integer-overflow.yml b/data/rules/gitlab-sast-rules/go/memory/rule-integer-overflow.yml new file mode 100644 index 0000000..b8d48df --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/memory/rule-integer-overflow.yml @@ -0,0 +1,51 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/integer_overflow.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_memory_rule-integer-overflow" + languages: + - "go" + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + $X, ... := strconv.Atoi(...) + ... + - pattern-either: + - pattern: "int32($X)" + - pattern: "int16($X)" + message: | + Golang's `int` type size depends on the architecture of where the application is running. For + 32-bit systems, `int` is + 32-bit, for 64-bit systems, `int` will be 64-bit. By calling `strconv.Atoi` with a large + number, the integer may overflow + if the `int` return value is type converted into a smaller type (`int32` or `int16`). This + could cause unexpected application + behavior depending on how the resultant value is used. + + Prior to running any type conversion, check that the value returned from `strconv.Atoi` will + fit in the resulting integer. + + Example of checking the return value before type conversion: + ``` + bigValue, _ := strconv.Atoi("32768") + if bigValue > math.MaxInt16 { + log.Fatal("value too large to fit in int16") + } + value := int16(bigValue) + fmt.Println(value) + ``` + + For more information on integer min/max constants see: https://pkg.go.dev/math#pkg-constants + metadata: + shortDescription: "Integer overflow or wraparound" + cwe: "CWE-190" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + security-severity: "Medium" + category: "security" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/go/memory/rule-memoryaliasing.yml b/data/rules/gitlab-sast-rules/go/memory/rule-memoryaliasing.yml new file mode 100644 index 0000000..5814203 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/memory/rule-memoryaliasing.yml @@ -0,0 +1,86 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/implicit_aliasing.go +# yamllint enable +--- +rules: +- id: "go_memory_rule-memoryaliasing" + languages: + - "go" + patterns: + - pattern-either: + - pattern: | + for ..., $ARG := range $SLICE { + <... &($ARG) ...> + } + - pattern: | + for ..., $ARG := range $SLICE { + <... func() { <... &$ARG ...> } ...> + } + - pattern: | + for ..., $ARG := range $SLICE { + <... $X(..., <... &$ARG ...>, ...) ...> + } + - pattern-not: | + for ..., $ARG := range $SLICE { + <... *$ARG ...> + } + - pattern-not-inside: |- + for ..., $ARG := range $SLICE { return ... } + message: | + Go's `for ... range` statements create an iteration variable for each iteration of the loop. + By taking the address of this iteration variable, the value of the address will be re-used + and always point to the same location in memory. This can have unexpected behavior if the + address is stored or re-used. + + This can be fixed by: + - Not referencing the address of the variable + - Re-assigning the iteration variable to a new variable + - Using the address of the indexed variable + + Example not referencing the address: + ``` + type someStruct struct { + x int + } + + for _, n := range []someStruct{{1}, {2}, {3}, {4}} { + fmt.Printf("%d\n", n.x) + } + ``` + + Example reassigning the iteration variable to a new variable: + ``` + type someStruct struct { + x int + } + + for _, n := range []someStruct{{1}, {2}, {3}, {4}} { + p := n + fmt.Printf("%p\n", &p) + } + ``` + + Example using the address of the indexed variable: + ``` + type someStruct struct { + x int + } + + structData := []someStruct{{1}, {2}, {3}, {4}} + for idx := range structData { + fmt.Printf("%p\n", &structData[idx]) + } + ``` + + For more information on how the `for ... range` statement works see: + https://go.dev/ref/spec#For_statements + metadata: + shortDescription: "Incorrect access of indexable resource ('Range Error')" + cwe: "CWE-118" + owasp: + - "A6:2017-Security Misconfiguration" + - "A05:2021-Security Misconfiguration" + security-severity: "Info" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/network/rule-bind-to-all-interfaces.yml b/data/rules/gitlab-sast-rules/go/network/rule-bind-to-all-interfaces.yml new file mode 100644 index 0000000..534eb44 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/network/rule-bind-to-all-interfaces.yml @@ -0,0 +1,44 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/bind.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_network_rule-bind-to-all-interfaces" + languages: + - "go" + patterns: + - pattern-either: + - pattern: "net.Listen(..., \"$ADDR\")" + - pattern: "tls.Listen(..., \"$ADDR\", ...)" + - metavariable-regex: + metavariable: "$ADDR" + regex: "^(0\\.0\\.0\\.0|\\[::\\])?(:[0-9]*)?$" + message: | + Binding to all network interfaces can potentially open up a service to + traffic on unintended interfaces, that may not be properly documented or + secured. By passing "0.0.0.0" as the address to the `Listen` family of functions, + the application will bind to all interfaces. + + Consider passing in the interface ip address through an environment variable, + configuration file, or by determining the primary interface(s) IP address. + + Example getting the IP address from an environment variable `IP_ADDRESS`: + ``` + addr := os.Getenv("IP_ADDRESS") + listener, err := net.Listen("tcp", addr) + if err != nil { + log.Fatal(err) + } + ``` + metadata: + shortDescription: "Binding to an unrestricted IP address" + cwe: "CWE-1327" + owasp: + - "A6:2017-Security Misconfiguration" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Low" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/sql/rule-concat-sqli.yml b/data/rules/gitlab-sast-rules/go/sql/rule-concat-sqli.yml new file mode 100644 index 0000000..3997ad3 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/sql/rule-concat-sqli.yml @@ -0,0 +1,72 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/sql.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_sql_rule-concat-sqli" + languages: + - "go" + mode: taint + pattern-sources: + - patterns: + - pattern: "fmt.Sprintf(...)" + - pattern-not: | + fmt.Sprintf("...", "...") + - patterns: + - pattern: | + "..." + $X + - pattern-not: | + "..." + "..." + - pattern: | + ($SB : strings.Builder).String() + pattern-sinks: + - patterns: + - pattern: "$DB.$METHOD(...)" + - metavariable-regex: + metavariable: $METHOD + regex: "^(Exec(Context)?|Query(Context)?|QueryRow(Context)?)$" + message: | + SQL Injection is a critical vulnerability that can lead to data or system compromise. By + dynamically generating SQL query strings, user input may be able to influence the logic of + the SQL statement. This could lead to an adversary accessing information they should + not have access to or in some circumstances, being able to execute OS functionality or code. + + Replace all dynamically generated SQL queries with parameterized queries. In situations where + dynamic queries must be created, never use direct user input, but instead use a map or + dictionary of valid values and resolve them using a user supplied key. + + For example, some database drivers do not allow parameterized queries for `>` or `<` comparison + operators. In these cases, do not use a user supplied `>` or `<` value, but rather have the + user + supply a `gt` or `lt` value. The alphabetical values are then used to look up the `>` and `<` + values to be used in the construction of the dynamic query. The same goes for other queries + where + column or table names are required but cannot be parameterized. + + Example using parameterized queries with `sql.Query`: + ``` + rows, err := db.Query("SELECT * FROM users WHERE userName = ?", userName) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + // ... process rows + } + ``` + + For more information on SQL Injection see OWASP: + https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html + metadata: + shortDescription: "Improper neutralization of special elements used in an SQL + command ('SQL Injection')" + cwe: "CWE-89" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + security-severity: "High" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/subproc/rule-subproc.yml b/data/rules/gitlab-sast-rules/go/subproc/rule-subproc.yml new file mode 100644 index 0000000..88937e8 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/subproc/rule-subproc.yml @@ -0,0 +1,89 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (modified): https://github.com/securego/gosec/blob/master/rules/subproc.go +# hash: d3309fb +# yamllint enable +--- +rules: +- id: "go_subproc_rule-subproc" + languages: + - "go" + patterns: + - pattern-either: + - patterns: + - pattern: "exec.CommandContext($CTX, $EXE, ...)" + - pattern-not: "exec.CommandContext($CTX, \"...\", ...)" + - patterns: + - pattern: "exec.Command($EXE, ...)" + - pattern-not: "exec.Command(\"...\", ...)" + - patterns: + - pattern: "syscall.ForkExec($EXE, ...)" + - pattern-not: "syscall.ForkExec(\"...\", ...)" + - patterns: + - pattern: "syscall.StartProcess($EXE, ...)" + - pattern-not: "syscall.StartProcess(\"...\", ...)" + message: | + OS command injection is a critical vulnerability that can lead to a full system + compromise as it may allow an adversary to pass in arbitrary commands or arguments + to be executed. + + User input should never be used in constructing commands or command arguments + to functions which execute OS commands. This includes filenames supplied by + user uploads or downloads. + + Ensure your application does not: + + - Use user-supplied information in the process name to execute. + - Use user-supplied information in an OS command execution function which does + not escape shell meta-characters. + - Use user-supplied information in arguments to OS commands. + + The application should have a hardcoded set of arguments that are to be passed + to OS commands. If filenames are being passed to these functions, it is + recommended that a hash of the filename be used instead, or some other unique + identifier. It is strongly recommended that a native library that implements + the same functionality be used instead of using OS system commands, due to the + risk of unknown attacks against third party commands. + + If operating in Windows environments, when specifying the OS command, ensure + the application uses the full path + information, otherwise the OS may attempt to look up which process to execute + and could be vulnerable to untrusted search path vulnerabilities (CWE-426). + + Example of safely executing an OS command: + ``` + userData := []byte("user data") + // create a temporary file in the application specific directory + f, err := ioutil.TempFile("/var/app/restricted", "temp-*.dat") + if err != nil { + log.Fatal(err) + } + + if _, err := f.Write(userData); err != nil { + log.Fatal(err) + } + + if err := f.Close(); err != nil { + log.Fatal(err) + } + + // pass the full path to the binary and the name of the temporary file + // instead of any user supplied filename + out, err := exec.Command("/bin/cat", f.Name()).Output() + if err != nil { + log.Fatal(err) + } + ``` + + For more information on OS command injection, see OWASP's guide: + https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html + metadata: + shortDescription: "Improper neutralization of special elements used in an OS command ('OS Command Injection')" + cwe: "CWE-78" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + security-severity: "High" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/go/unsafe/rule-unsafe.yml b/data/rules/gitlab-sast-rules/go/unsafe/rule-unsafe.yml new file mode 100644 index 0000000..f6fecc9 --- /dev/null +++ b/data/rules/gitlab-sast-rules/go/unsafe/rule-unsafe.yml @@ -0,0 +1,42 @@ +# yamllint disable +# License: Apache 2.0 (c) gosec +# source (original): https://github.com/securego/gosec/blob/master/rules/unsafe.go +# hash: c6e10af +# yamllint enable +--- +rules: +- id: "go_unsafe_rule-unsafe" + languages: + - "go" + patterns: + - pattern-either: + - pattern: "unsafe.Alignof(...)" + - pattern: "unsafe.Offsetof(...)" + - pattern: "unsafe.Sizeof(...)" + - pattern: "unsafe.Pointer(...)" + message: | + The `unsafe` package in Go allows low-level access to memory management features. + This includes pointers and direct access to memory. The Go compiler will no longer + be able to enforce type safety when working with the `unsafe` pointer types. + + While powerful, access to these functions can lead to many security related issues + such as: + + - [Buffer overflows](https://owasp.org/www-community/vulnerabilities/Buffer_Overflow) which + can lead to code execution. + - [Use after free](https://owasp.org/www-community/vulnerabilities/Using_freed_memory) which + can lead to code execution. + - [Information/Memory leaks](https://owasp.org/www-community/vulnerabilities/Memory_leak) + which can leak sensitive information, including data which can + defeat other protection mechanisms or cause the system to run out of memory. + + Unless required, all calls to the `unsafe` package should be removed. + metadata: + shortDescription: "Use of inherently dangerous function (unsafe package)" + cwe: "CWE-242" + owasp: + - "A6:2017-Security Misconfiguration" + - "A04:2021-Insecure Design" + security-severity: "High" + category: "security" + severity: "INFO" diff --git a/data/rules/gitlab-sast-rules/java/cookie/rule-RequestParamToHeader.yml b/data/rules/gitlab-sast-rules/java/cookie/rule-RequestParamToHeader.yml new file mode 100644 index 0000000..a4a01e1 --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/cookie/rule-RequestParamToHeader.yml @@ -0,0 +1,93 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_cookie_rule-RequestParamToHeader" + languages: + - "java" + mode: "taint" + pattern-sanitizers: + - patterns: + - pattern-inside: | + $STR.replaceAll("$INPUT", "..."); + ... + - pattern: "$STR" + - metavariable-regex: + metavariable: "$INPUT" + regex: ".*\\[(?=.*\\\\r)(?=.*\\\\n).*\\]\\+" + - pattern: "org.apache.commons.text.StringEscapeUtils.unescapeJava(...);" + pattern-sinks: + - pattern: "($X.servlet.http.HttpServletResponse $RES).setHeader(\"$KEY\", ...);" + - pattern: "($X.servlet.http.HttpServletResponse $RES).addHeader(\"$KEY\", ...);" + - pattern: "($X.servlet.http.HttpServletResponseWrapper $WRP).setHeader(\"$KEY\", + ...);" + - pattern: "($X.servlet.http.HttpServletResponseWrapper $WRP).addHeader(\"$KEY\", + ...);" + pattern-sources: + - pattern: "($X.servlet.http.HttpServletRequest $REQ).getParameter(...);" + - pattern: "($X.servlet.http.HttpServletRequest $REQ).getParameterNames();" + - pattern: "($X.servlet.http.HttpServletRequest $REQ).getParameterValues(...);" + - pattern: "($X.servlet.http.HttpServletRequest $REQ).getParameterMap();" + - pattern: "($X.servlet.http.HttpServletRequest $REQ).getHeader(...);" + - pattern: "($X.servlet.http.HttpServletRequest $REQ).getPathInfo();" + message: | + HTTP Response Splitting is a vulnerability where Carriage Return (CR `\r`) and Line Feed (LF + `\n`) + characters are introduced into an HTTP header from user-supplied input. By injecting the + `\r\n` + character sequence, an adversary could potentially modify how the response is interpreted by + the + client or any down stream caching services. This could allow an adversary to poison the cache + data or execute Cross-Site Scripting (XSS) attacks. + + Some Java application servers such as [Apache Tomcat](https://tomcat.apache.org/) will + automatically encode + characters from being set in response headers as a space `0x20` character. If your application + server does + not automatically provide this functionality, user-supplied input that is used in header keys + or values must be + validated. + + Example of validating headers to only allow valid characters: + ``` + // throws an IllegalArgumentException if the provided value contains invalid characters + public void validateHeader(String value) throws IllegalArgumentException { + char[] chars = value.toCharArray(); + + // iterate over every character + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + + // check for any characters below 0x21 as well as: '"' ',' ';' '\' and 0x7f. + if (c < 0x21 || c == '"' || c == ',' || c == ';' || c == '\\' || c == 0x7f) { + throw new IllegalArgumentException("Invalid character in cookie detected: + {0}".format(Integer.toString(c))); + } + } + } + ``` + + Alternatively, you could use a string escape package such as + [Apache Commons Text](https://commons.apache.org/proper/commons-text/) to escape the input: + ``` + public String escapeValue(String value) { + return StringEscapeUtils.escapeJava(value); + } + ``` + + For more information on response splitting attacks see OWASP: + https://owasp.org/www-community/attacks/HTTP_Response_Splitting + severity: "ERROR" + metadata: + shortDescription: "Improper neutralization of CRLF sequences in HTTP headers ('HTTP + Response Splitting')" + category: "security" + cwe: "CWE-113" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + technology: + - "java" + security-severity: "High" diff --git a/data/rules/gitlab-sast-rules/java/cors/rule-PermissiveCORSInjection.yml b/data/rules/gitlab-sast-rules/java/cors/rule-PermissiveCORSInjection.yml new file mode 100644 index 0000000..ba997e4 --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/cors/rule-PermissiveCORSInjection.yml @@ -0,0 +1,80 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_cors_rule-PermissiveCORSInjection" + languages: + - "java" + mode: "taint" + pattern-sources: + - pattern: "(HttpServletRequest $REQ).getParameter(...)" + - pattern: "(HttpServletRequest $REQ).getHeader(...)" + - pattern: "(HttpServletRequest $REQ).getPathInfo()" + - pattern: "(HttpServletRequest $REQ).getQueryString()" + - pattern: "(HttpServletRequest $REQ).getAttribute(...)" + - pattern: "(HttpServletRequest $REQ).getSession().getAttribute(...)" + - pattern: "(HttpServletRequest $REQ).getServletContext().getAttribute(...)" + - pattern: "(HttpServletRequest $REQ).getParameterValues(...)" + - pattern: "(HttpServletRequest $REQ).getParameterNames()" + - pattern: "(HttpServletRequest $REQ).getParameterMap()" + pattern-sinks: + - patterns: + - pattern-either: + - pattern: "(HttpServletResponse $RES).setHeader(\"$HEADER\", ...)" + - pattern: "(HttpServletResponse $RES).addHeader(\"$HEADER\", ...)" + - metavariable-regex: + metavariable: "$HEADER" + regex: "(?i)(Access-Control-Allow-Origin)" + message: | + This application potentially allows user-supplied input into the value of the + `Access-Control-Allow-Origin` response header. This header is part of the + [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) CORS + specification. By allowing user input to specify which domains can communicate with this + server, + an adversary could exploit a weakness in this server to force clients to send credentials (such + as session + identifiers) to the adversary's server. + + For the above attack to work, the application would need to suffer from an additional + vulnerability, + such as Cross-Site Scripting (XSS). + + To remediate this issue, do not use user-supplied information when calling + `HttpServletResponse.setHeader` or `HttpServletResponse.addHeader` + for the `Access-Control-Allow-Origin` header's value. Instead, hardcode the allowed domain(s) + and reference them in a lookup + table: + Example allowing dynamic but safe domains in `Access-Control-Allow-Origin`: + + ``` + // this data should be in the class constructor or taken from a trusted datasource + Map allowedDomains = new HashMap(); + allowedDomains.put("sub1", "sub1.example.com"); + allowedDomains.put("sub2", "sub2.example.com"); + + // extract the allowedDomain parameters value as a key to look up which domain to provide + via the allowedDomains map + // if not found, sets sub1 as the default + String headerValue = allowedDomains.getOrDefault(request.getParameter("allowedDomain"), + allowedDomains.get("sub1")); + + // add the header with our trusted sub1.example.com or sub2.example.com domains. + response.addHeader("Access-Control-Allow-Origin", headerValue); + } + ``` + + For more information on `Access-Control-Allow-Origin` see: + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + severity: "ERROR" + metadata: + shortDescription: "Permissive cross-domain policy with untrusted domains" + cwe: "CWE-942" + category: "security" + owasp: + - "A5:2017-Broken Access Control" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + technology: + - "java" + security-severity: "Low" diff --git a/data/rules/gitlab-sast-rules/java/crypto/rule-CipherDESInsecure.yml b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherDESInsecure.yml new file mode 100644 index 0000000..311f713 --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherDESInsecure.yml @@ -0,0 +1,118 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_crypto_rule-CipherDESInsecure" + languages: + - "java" + patterns: + - pattern-either: + - pattern: javax.crypto.Cipher.getInstance("$ALG", ...) + - pattern: | + $PROP = (java.util.Properties $P).getProperty(..., "$ALG"); + ... + javax.crypto.Cipher.getInstance($PROP, ...); + - metavariable-regex: + metavariable: $ALG + regex: "^DES(/|$)" + message: | + DES, TripleDES and RC2 are all considered broken or insecure cryptographic algorithms. + Newer algorithms apply message integrity to validate ciphertext has not been tampered + with. Consider using `ChaCha20Poly1305` instead as it is easier and faster than the + alternatives such as `AES-256-GCM`. + + For older applications that don't have support for `ChaCha20Poly1305`, + `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + public encrypt() throws Exception { + chaChaEncryption("Secret text to encrypt".getBytes(StandardCharsets.UTF_8)); + } + + public SecureRandom getSecureRandomDRBG() throws NoSuchAlgorithmException { + // Use DRBG according to + http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf + return SecureRandom.getInstance("DRBG", + // Security strength in bits (default is 128) + DrbgParameters.instantiation(256, + // Set prediction resistance and re-seeding + DrbgParameters.Capability.PR_AND_RESEED, + // Set the personalization string (optional, not necessary) + "some_personalization_string".getBytes() + ) + ); + } + + public Cipher getChaCha20Poly1305(int mode, byte[] ivKey, byte[] secretKey) throws + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create a ChaCha20-Poly1305 cipher instance + Cipher chaChaCipher = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding"); + // Create our parameterSpec using our ivKey + AlgorithmParameterSpec parameterSpec = new IvParameterSpec(ivKey); + // Create a SecretKeySpec using our secretKey + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "ChaCha20"); + // Initialize and return the cipher for the provided mode + chaChaCipher.init(mode, secretKeySpec, parameterSpec, random); + return chaChaCipher; + } + + public void chaChaEncryption(byte[] plainText) throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create secretKey + byte[] secretKey = new byte[32]; + random.nextBytes(secretKey); + // Create an IV Key + byte[] ivKey = new byte[12]; + random.nextBytes(ivKey); + + // Create a chaCha encryption cipher instance + Cipher chaChaEncryptor = getChaCha20Poly1305(Cipher.ENCRYPT_MODE, ivKey, secretKey); + + // Encrypt the text using ChaCha20Poly1305 + byte[] cipherText = null; + try { + cipherText = chaChaEncryptor.doFinal(plainText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to encrypt text"); + return; + } + System.out.println("encrypted: " + Base64.getEncoder().encodeToString(cipherText)); + + // Create a chaCha decryption cipher instance + Cipher chaChaDecryptor = getChaCha20Poly1305(Cipher.DECRYPT_MODE, ivKey, secretKey); + + // Decrypt the text + byte[] decryptedText = null; + try { + decryptedText = chaChaDecryptor.doFinal(cipherText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to decrypt text"); + return; + } + System.out.println("decrypted: " + new String(decryptedText, StandardCharsets.UTF_8)); + } + ``` + + For more information on Java Cryptography see: + https://docs.oracle.com/en/java/javase/15/security/java-cryptography-architecture-jca-reference-guide.html + severity: "WARNING" + metadata: + shortDescription: "Inadequate encryption strength" + category: "security" + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + technology: + - "java" + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/java/crypto/rule-CipherDESedeInsecure.yml b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherDESedeInsecure.yml new file mode 100644 index 0000000..7a7c4bc --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherDESedeInsecure.yml @@ -0,0 +1,114 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_crypto_rule-CipherDESedeInsecure" + languages: + - "java" + patterns: + - pattern-either: + - pattern: javax.crypto.Cipher.getInstance("$ALG", ...) + - pattern: | + $PROP = (java.util.Properties $P).getProperty(..., "$ALG"); + ... + javax.crypto.Cipher.getInstance($PROP, ...); + - metavariable-regex: + metavariable: $ALG + regex: "DESede(/|$)" + message: | + DES, TripleDES and RC2 are all considered broken or insecure cryptographic algorithms. + Newer algorithms apply message integrity to validate ciphertext has not been tampered + with. Consider using `ChaCha20Poly1305` instead as it is easier and faster than the + alternatives such as `AES-256-GCM`. + + For older applications that don't have support for `ChaCha20Poly1305`, + `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + public encrypt() throws Exception { + chaChaEncryption("Secret text to encrypt".getBytes(StandardCharsets.UTF_8)); + } + + public SecureRandom getSecureRandomDRBG() throws NoSuchAlgorithmException { + // Use DRBG according to + http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf + return SecureRandom.getInstance("DRBG", + // Security strength in bits (default is 128) + DrbgParameters.instantiation(256, + // Set prediction resistance and re-seeding + DrbgParameters.Capability.PR_AND_RESEED, + // Set the personalization string (optional, not necessary) + "some_personalization_string".getBytes() + ) + ); + } + + public Cipher getChaCha20Poly1305(int mode, byte[] nonceKey, byte[] secretKey) throws + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create a ChaCha20-Poly1305 cipher instance + Cipher chaChaCipher = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding"); + // Create our parameterSpec using our nonceKey + AlgorithmParameterSpec parameterSpec = new IvParameterSpec(nonceKey); + // Create a SecretKeySpec using our secretKey + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "ChaCha20"); + // Initialize and return the cipher for the provided mode + chaChaCipher.init(mode, secretKeySpec, parameterSpec, random); + return chaChaCipher; + } + + public void chaChaEncryption(byte[] plainText) throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create secretKey + byte[] secretKey = new byte[32]; + random.nextBytes(secretKey); + // Create an IV nonceKey + byte[] nonceKey = new byte[12]; + random.nextBytes(nonceKey); + // Create a chaCha encryption cipher instance + Cipher chaChaEncryptor = getChaCha20Poly1305(Cipher.ENCRYPT_MODE, nonceKey, secretKey); + // Encrypt the text using ChaCha20Poly1305 + byte[] cipherText = null; + try { + cipherText = chaChaEncryptor.doFinal(plainText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to encrypt text"); + return; + } + System.out.println("encrypted: " + Base64.getEncoder().encodeToString(cipherText)); + // Create a chaCha decryption cipher instance + Cipher chaChaDecryptor = getChaCha20Poly1305(Cipher.DECRYPT_MODE, nonceKey, secretKey); + // Decrypt the text + byte[] decryptedText = null; + try { + decryptedText = chaChaDecryptor.doFinal(cipherText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to decrypt text"); + return; + } + System.out.println("decrypted: " + new String(decryptedText, StandardCharsets.UTF_8)); + } + ``` + + For more information on Java Cryptography see: + https://docs.oracle.com/en/java/javase/15/security/java-cryptography-architecture-jca-reference-guide.html + severity: "WARNING" + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + category: "security" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + technology: + - "java" + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/java/crypto/rule-CipherECBMode.yml b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherECBMode.yml new file mode 100644 index 0000000..88350d2 --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherECBMode.yml @@ -0,0 +1,129 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_crypto_rule-CipherECBMode" + languages: + - "java" + patterns: + - pattern-either: + - pattern: javax.crypto.Cipher.getInstance("$ALG", ...) + - pattern: | + $PROP = (java.util.Properties $P).getProperty(..., "$ALG"); + ... + javax.crypto.Cipher.getInstance($PROP, ...); + - metavariable-comparison: + metavariable: $ALG + comparison: | + $ALG in ( + "AES/ECB/NoPadding" "AES/ECB/PKCS5Padding" + "DES/ECB/NoPadding" "DES/ECB/PKCS5Padding" + "DESede/ECB/NoPadding" "DESede/ECB/PKCS5Padding" + "AES/ECB/PKCS7Padding" + ) + message: | + Cryptographic algorithms provide many different modes of operation, only some of which provide + message integrity. Without message integrity it could be possible for an adversary to attempt + to tamper with the ciphertext which could lead to compromising the encryption key. Newer + algorithms + apply message integrity to validate ciphertext has not been tampered with. + + Instead of using an algorithm that requires configuring a cipher mode, an algorithm + that has built-in message integrity should be used. Consider using `ChaCha20Poly1305` or + `AES-256-GCM` instead. + + For older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is + recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + public encrypt() throws Exception { + chaChaEncryption("Secret text to encrypt".getBytes(StandardCharsets.UTF_8)); + } + + public SecureRandom getSecureRandomDRBG() throws NoSuchAlgorithmException { + // Use DRBG according to + http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf + return SecureRandom.getInstance("DRBG", + // Security strength in bits (default is 128) + DrbgParameters.instantiation(256, + // Set prediction resistance and re-seeding + DrbgParameters.Capability.PR_AND_RESEED, + // Set the personalization string (optional, not necessary) + "some_personalization_string".getBytes() + ) + ); + } + + public Cipher getChaCha20Poly1305(int mode, byte[] ivKey, byte[] secretKey) throws + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create a ChaCha20-Poly1305 cipher instance + Cipher chaChaCipher = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding"); + // Create our parameterSpec using our ivKey + AlgorithmParameterSpec parameterSpec = new IvParameterSpec(ivKey); + // Create a SecretKeySpec using our secretKey + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "ChaCha20"); + // Initialize and return the cipher for the provided mode + chaChaCipher.init(mode, secretKeySpec, parameterSpec, random); + return chaChaCipher; + } + + public void chaChaEncryption(byte[] plainText) throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create secretKey + byte[] secretKey = new byte[32]; + random.nextBytes(secretKey); + // Create an IV Key + byte[] ivKey = new byte[12]; + random.nextBytes(ivKey); + + // Create a chaCha encryption cipher instance + Cipher chaChaEncryptor = getChaCha20Poly1305(Cipher.ENCRYPT_MODE, ivKey, secretKey); + + // Encrypt the text using ChaCha20Poly1305 + byte[] cipherText = null; + try { + cipherText = chaChaEncryptor.doFinal(plainText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to encrypt text"); + return; + } + System.out.println("encrypted: " + Base64.getEncoder().encodeToString(cipherText)); + + // Create a chaCha decryption cipher instance + Cipher chaChaDecryptor = getChaCha20Poly1305(Cipher.DECRYPT_MODE, ivKey, secretKey); + + // Decrypt the text + byte[] decryptedText = null; + try { + decryptedText = chaChaDecryptor.doFinal(cipherText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to decrypt text"); + return; + } + System.out.println("decrypted: " + new String(decryptedText, StandardCharsets.UTF_8)); + } + ``` + + For more information on Java Cryptography see: + https://docs.oracle.com/en/java/javase/15/security/java-cryptography-architecture-jca-reference-guide.html + severity: "ERROR" + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + category: "security" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + technology: + - "java" + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/java/crypto/rule-CipherIntegrity.yml b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherIntegrity.yml new file mode 100644 index 0000000..95f99bc --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherIntegrity.yml @@ -0,0 +1,139 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_crypto_rule-CipherIntegrity" + languages: + - "java" + patterns: + - pattern-either: + - pattern: javax.crypto.Cipher.getInstance("$ALG", ...) + - pattern: | + $PROP = (java.util.Properties $P).getProperty(..., "$ALG"); + ... + javax.crypto.Cipher.getInstance($PROP, ...); + - metavariable-comparison: + metavariable: $ALG + comparison: | + $ALG in ( + "AES" + "AES/CBC/NoPadding" "AES/CBC/PKCS5Padding" "AES/CBC/PKCS7Padding" + "AES/CFB/NoPadding" "AES/CFB/PKCS5Padding" "AES/CFB/PKCS7Padding" + "AES/CTR/NoPadding" + "AES/ECB/NoPadding" "AES/ECB/PKCS5Padding" "AES/ECB/PKCS7Padding" + "AES/OFB/NoPadding" "AES/OFB/PKCS5Padding" "AES/OFB/PKCS7Padding" + "ARCFOUR" + "DES" + "DES/CBC/NoPadding" "DES/CBC/PKCS5Padding" + "DES/ECB/NoPadding" "DES/ECB/PKCS5Padding" + "DESede" + "DESede/CBC/NoPadding" "DESede/CBC/PKCS5Padding" + "DESede/ECB/NoPadding" "DESede/ECB/PKCS5Padding" + "RC4" + ) + message: | + Cryptographic algorithms provide many different modes of operation, only some of which provide + message integrity. Without message integrity it could be possible for an adversary to attempt + to tamper with the ciphertext which could lead to compromising the encryption key. Newer + algorithms + apply message integrity to validate ciphertext has not been tampered with. + + Instead of using an algorithm that requires configuring a cipher mode, an algorithm + that has built-in message integrity should be used. Consider using `ChaCha20Poly1305` or + `AES-256-GCM` instead. + + For older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is + recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + public encrypt() throws Exception { + chaChaEncryption("Secret text to encrypt".getBytes(StandardCharsets.UTF_8)); + } + + public SecureRandom getSecureRandomDRBG() throws NoSuchAlgorithmException { + // Use DRBG according to + http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf + return SecureRandom.getInstance("DRBG", + // Security strength in bits (default is 128) + DrbgParameters.instantiation(256, + // Set prediction resistance and re-seeding + DrbgParameters.Capability.PR_AND_RESEED, + // Set the personalization string (optional, not necessary) + "some_personalization_string".getBytes() + ) + ); + } + + public Cipher getChaCha20Poly1305(int mode, byte[] ivKey, byte[] secretKey) throws + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create a ChaCha20-Poly1305 cipher instance + Cipher chaChaCipher = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding"); + // Create our parameterSpec using our ivKey + AlgorithmParameterSpec parameterSpec = new IvParameterSpec(ivKey); + // Create a SecretKeySpec using our secretKey + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "ChaCha20"); + // Initialize and return the cipher for the provided mode + chaChaCipher.init(mode, secretKeySpec, parameterSpec, random); + return chaChaCipher; + } + + public void chaChaEncryption(byte[] plainText) throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create secretKey + byte[] secretKey = new byte[32]; + random.nextBytes(secretKey); + // Create an IV Key + byte[] ivKey = new byte[12]; + random.nextBytes(ivKey); + + // Create a chaCha encryption cipher instance + Cipher chaChaEncryptor = getChaCha20Poly1305(Cipher.ENCRYPT_MODE, ivKey, secretKey); + + // Encrypt the text using ChaCha20Poly1305 + byte[] cipherText = null; + try { + cipherText = chaChaEncryptor.doFinal(plainText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to encrypt text"); + return; + } + System.out.println("encrypted: " + Base64.getEncoder().encodeToString(cipherText)); + + // Create a chaCha decryption cipher instance + Cipher chaChaDecryptor = getChaCha20Poly1305(Cipher.DECRYPT_MODE, ivKey, secretKey); + + // Decrypt the text + byte[] decryptedText = null; + try { + decryptedText = chaChaDecryptor.doFinal(cipherText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to decrypt text"); + return; + } + System.out.println("decrypted: " + new String(decryptedText, StandardCharsets.UTF_8)); + } + ``` + + For more information on Java Cryptography see: + https://docs.oracle.com/en/java/javase/15/security/java-cryptography-architecture-jca-reference-guide.html + severity: "ERROR" + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + category: "security" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + technology: + - "java" + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/java/crypto/rule-CipherPaddingOracle.yml b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherPaddingOracle.yml new file mode 100644 index 0000000..c15ac4d --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/crypto/rule-CipherPaddingOracle.yml @@ -0,0 +1,132 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_crypto_rule-CipherPaddingOracle" + languages: + - "java" + patterns: + - pattern-either: + - pattern: javax.crypto.Cipher.getInstance("$ALG", ...) + - pattern: | + $PROP = (java.util.Properties $P).getProperty(..., "$ALG"); + ... + javax.crypto.Cipher.getInstance($PROP, ...); + - metavariable-comparison: + metavariable: $ALG + comparison: | + $ALG in ( + "AES/CBC/PKCS5Padding" "DES/CBC/PKCS5Padding" "DESede/CBC/PKCS5Padding" "AES/CBC/PKCS7Padding" + ) + message: | + Cryptographic block ciphers can be configured to pad individual blocks if there is not enough + input data to match the size of the block. This specific mode of CBC used in combination with + PKCS5Padding is susceptible to padding oracle attacks. An adversary could potentially decrypt + the message if the system exposed the difference between plaintext with invalid padding or + valid padding. The distinction between valid and invalid padding is usually revealed through + distinct error messages being returned for each condition. + + Consider switching to a more secure cipher that doesn't require padding and builds in message + authentication integrity directly into the algorithm. + + Consider using `ChaCha20Poly1305` or + `AES-256-GCM` instead. + + For older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is + recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + public encrypt() throws Exception { + chaChaEncryption("Secret text to encrypt".getBytes(StandardCharsets.UTF_8)); + } + + public SecureRandom getSecureRandomDRBG() throws NoSuchAlgorithmException { + // Use DRBG according to + http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf + return SecureRandom.getInstance("DRBG", + // Security strength in bits (default is 128) + DrbgParameters.instantiation(256, + // Set prediction resistance and re-seeding + DrbgParameters.Capability.PR_AND_RESEED, + // Set the personalization string (optional, not necessary) + "some_personalization_string".getBytes() + ) + ); + } + + public Cipher getChaCha20Poly1305(int mode, byte[] ivKey, byte[] secretKey) throws + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create a ChaCha20-Poly1305 cipher instance + Cipher chaChaCipher = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding"); + // Create our parameterSpec using our ivKey + AlgorithmParameterSpec parameterSpec = new IvParameterSpec(ivKey); + // Create a SecretKeySpec using our secretKey + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "ChaCha20"); + // Initialize and return the cipher for the provided mode + chaChaCipher.init(mode, secretKeySpec, parameterSpec, random); + return chaChaCipher; + } + + public void chaChaEncryption(byte[] plainText) throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { + // Get a DRBG random number generator instance + SecureRandom random = getSecureRandomDRBG(); + // Create secretKey + byte[] secretKey = new byte[32]; + random.nextBytes(secretKey); + // Create an IV Key + byte[] ivKey = new byte[12]; + random.nextBytes(ivKey); + + // Create a chaCha encryption cipher instance + Cipher chaChaEncryptor = getChaCha20Poly1305(Cipher.ENCRYPT_MODE, ivKey, secretKey); + + // Encrypt the text using ChaCha20Poly1305 + byte[] cipherText = null; + try { + cipherText = chaChaEncryptor.doFinal(plainText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to encrypt text"); + return; + } + System.out.println("encrypted: " + Base64.getEncoder().encodeToString(cipherText)); + + // Create a chaCha decryption cipher instance + Cipher chaChaDecryptor = getChaCha20Poly1305(Cipher.DECRYPT_MODE, ivKey, secretKey); + + // Decrypt the text + byte[] decryptedText = null; + try { + decryptedText = chaChaDecryptor.doFinal(cipherText); + } catch (IllegalBlockSizeException | BadPaddingException e) { + System.out.println("failed to decrypt text"); + return; + } + System.out.println("decrypted: " + new String(decryptedText, StandardCharsets.UTF_8)); + } + ``` + + For more information on padding oracle attacks see: + https://en.wikipedia.org/wiki/Padding_oracle_attack + + For more information on Java Cryptography see: + https://docs.oracle.com/en/java/javase/15/security/java-cryptography-architecture-jca-reference-guide.html + severity: "ERROR" + metadata: + shortDescription: "Use of a broken or risky cryptographic algorithm" + category: "security" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + technology: + - "java" + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/java/crypto/rule-WeakTLSProtocolVersion.yml b/data/rules/gitlab-sast-rules/java/crypto/rule-WeakTLSProtocolVersion.yml new file mode 100644 index 0000000..4bd5fdf --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/crypto/rule-WeakTLSProtocolVersion.yml @@ -0,0 +1,90 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_crypto_rule-WeakTLSProtocolVersion" + languages: + - "java" + patterns: + - pattern-either: + - pattern-inside: | + import javax.net.ssl.*; + ... + - pattern-inside: | + import javax.net.ssl.SSLContext; + ... + - pattern-either: + - pattern-inside: | + SSLContext.getInstance("$UNSAFE_VERSION"); + - pattern-inside: | + SSLContext.getInstance(...); + ... + $ENGINE.setEnabledProtocols(new String[]{...,"$UNSAFE_VERSION",...}); + - pattern-not-inside: | + $C = SSLContext.getInstance(...); + ... + $ENGINE.setEnabledProtocols(new String[]{...,"TLSv1.2",...}); + - pattern-not-inside: | + $C = SSLContext.getInstance(...); + ... + $ENGINE.setEnabledProtocols(new String[]{...,"TLSv1.3",...}); + - pattern-not-inside: | + $C = SSLContext.getInstance(...); + ... + $ENGINE.setEnabledProtocols(new String[]{...,"DTLSv1.2",...}); + - pattern-not-inside: | + $C = SSLContext.getInstance(...); + ... + $ENGINE.setEnabledProtocols(new String[]{...,"DTLSv1.3",...}); + - metavariable-regex: + metavariable: "$UNSAFE_VERSION" + regex: "^(TLS|(D)?TLSv1.(0|1))$" + message: | + The application was found enabling insecure TLS protocol versions. When enabling protocol + versions for an `SSLContext`, only the following versions should be allowed: + - TLSv1.2 + - TLSv1.3 + - DTLSv1.2 + - DTLSv1.3 + + To mitigate potential security risks, it is strongly advised to enforce TLS 1.2 as the minimum + protocol version and disallow older versions such as TLS 1.0. Do note that newer versions of + Java do not even support TLS 1.0 and will throw `NoSuchAlgorithmException`. Versions of TLS + prior to 1.2 could expose the connection to downgrade attacks, where an adversary intercepts + the + connection and alters the requested protocol version to be a less secure one. + + In many scenarios, relying on the default system configuration does not meet compliance + standards. This is due to the application being deployed across diverse systems with varying + configurations and Java versions. While the default value may be secure on modern and + up-to-date systems, it may not hold true for older systems. Consequently, it is highly + recommended to explicitly define a secure configuration in all cases. + + Example configuring an SSLContext with TLSv1.2: + ``` + // Create an SSLContext with TLSv1.2 explicitly + SSLContext tlsContext = SSLContext.getInstance("TLSv1.2"); // or TLSv1.3, DTLSv1.2, DTLSv1.3 + + // Alternatively, set the enabled protocols + SSLContext serverSslContext = SSLContext.getInstance("TLS"); + SSLEngine serverEngine = serverSslContext.createSSLEngine(); + // Calling setEnabledProtocols will override the original context's configured protocol version + serverEngine.setEnabledProtocols(new String[]{ "TLSv1.2" }); + ``` + + For more information on `SSLContext` see: + - https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/net/ssl/SSLContext.html + + For more information on MiTM attacks see: + - https://owasp.org/www-community/attacks/Manipulator-in-the-middle_attack + metadata: + shortDescription: "Inadequate encryption strength" + category: "security" + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/java/inject/rule-ELInjection.yml b/data/rules/gitlab-sast-rules/java/inject/rule-ELInjection.yml new file mode 100644 index 0000000..bfd56ae --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/inject/rule-ELInjection.yml @@ -0,0 +1,93 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_inject_rule-ELInjection" + languages: + - "java" + message: | + This rule identifies potential Expression Language (EL) injection vulnerabilities within Java applications. + The rule targets use of `createValueExpression`, `createMethodExpression`, `ELProcessor.eval`, `getValue`, + and `setValue` methods, particularly when input to these methods is not a hardcoded string, indicating dynamic + evaluation of potentially untrusted input. + + `createValueExpression` creates a `ValueExpression` object which gets evaluated upon calling methods like + `getValue()` and `setValue()` or a Lambda `invoke()` i.e. it evaluates the expression passed to the + `createValueExpression` method. + + Similarly, `createMethodExpression` creates a `MethodExpression` object which gets evaluated upon calling + methods like `invoke()` and `getMethodInfo()`. + `ELProcessor.eval`, `getValue()`, and `setValue()` methods all evaluate their expressions which are passed + as parameters. + + Calling these method directly with user-supplied input may allow an adversary to execute arbitrary Java + code, including OS system commands. Never call these methods directly with user-supplied input. Consider + alternate methods such as a lookup table to take user input and resolve hardcoded values. + + Secure example: + + ``` + import javax.el.ELProcessor; + import java.util.Set; + + public class SafeELHandling { + private static final Set ALLOWED_VALUES = Set.of("value1", "value2", "value3"); + + public void processInput(String userInput) { + // Validate user input against the allowlist + if (!ALLOWED_VALUES.contains(userInput)) { + throw new IllegalArgumentException("Invalid input"); + } + + ELProcessor elProcessor = new ELProcessor(); + elProcessor.defineBean("userInput", userInput); + + // Example EL expression using the safe, predefined input + String result = (String) elProcessor.eval(userInput); + } + } + ``` + metadata: + category: "security" + cwe: "CWE-917" + shortDescription: "Improper neutralization of special elements used in an expression + language statement ('Expression Language Injection')" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + technology: + - "java" + security-severity: "Info" + pattern-either: + - patterns: + - pattern: | + (ExpressionFactory $EXP).createValueExpression((ELContext $CTX), $EXPR, + ...) + - pattern-not: | + (ExpressionFactory $EXP).createValueExpression((ELContext $CTX), "...", + ...) + - patterns: + - pattern: | + (ExpressionFactory $EXP).createMethodExpression((ELContext $CTX), $EXPR, + ...) + - pattern-not: | + (ExpressionFactory $EXP).createMethodExpression((ELContext $CTX), "...", + ...) + - patterns: + - pattern: | + ($X.el.ELProcessor $P).eval(...) + - pattern-not: | + ($X.el.ELProcessor $P).eval("...", ...) + - patterns: + - pattern: | + ($X.el.ELProcessor $P).getValue(...) + - pattern-not: | + ($X.el.ELProcessor $P).getValue("...", ...) + - patterns: + - pattern: | + ($X.el.ELProcessor $P).setValue(...) + - pattern-not: | + ($X.el.ELProcessor $P).setValue("...", "...") + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/java/password/rule-ConstantDBPassword.yml b/data/rules/gitlab-sast-rules/java/password/rule-ConstantDBPassword.yml new file mode 100644 index 0000000..43969ac --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/password/rule-ConstantDBPassword.yml @@ -0,0 +1,42 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_password_rule-ConstantDBPassword" + languages: + - "java" + patterns: + - pattern: "java.sql.DriverManager.getConnection($URI, $USR, \"...\");" + - pattern-not: "java.sql.DriverManager.getConnection($URI, $USR, \"\");" + message: | + A potential hard-coded password was identified in a database connection string. + Passwords should not be stored directly in code + but loaded from secure locations such as a Key Management System (KMS). + + The purpose of using a Key Management System is so access can be audited and keys easily + rotated + in the event of a breach. By hardcoding passwords, it will be extremely difficult to determine + when or if, a key is compromised. + + The recommendation on which KMS to use depends on the environment the application is running + in: + + - For Google Cloud Platform consider [Cloud Key Management](https://cloud.google.com/kms/docs) + - For Amazon Web Services consider [AWS Key Management](https://aws.amazon.com/kms/) + - For on premise or other alternatives to cloud providers, consider [Hashicorp's + Vault](https://www.vaultproject.io/) + - For other cloud providers, please see their documentation + severity: "ERROR" + metadata: + shortDescription: "Use of hard-coded password" + category: "security" + cwe: "CWE-259" + owasp: + - "A2:2017-Broken Authentication" + - "A07:2021-Identification and Authentication Failures" + - "A07:2025-Authentication Failures" + technology: + - "java" + security-severity: "Critical" + diff --git a/data/rules/gitlab-sast-rules/java/password/rule-EmptyDBPassword.yml b/data/rules/gitlab-sast-rules/java/password/rule-EmptyDBPassword.yml new file mode 100644 index 0000000..8058885 --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/password/rule-EmptyDBPassword.yml @@ -0,0 +1,45 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_password_rule-EmptyDBPassword" + languages: + - "java" + patterns: + - pattern: "java.sql.DriverManager.getConnection($URI, $USR, \"\");" + message: | + The application does not provide authentication when communicating a database + server. It is strongly recommended that the database server be configured with + authentication and restrict what queries users can execute. + + Please see your database server's documentation on how to configure a password. + + Additionally, passwords should not be stored directly in code + but loaded from secure locations such as a Key Management System (KMS). + + The purpose of using a Key Management System is so access can be audited and keys easily + rotated + in the event of a breach. By hardcoding passwords, it will be extremely difficult to determine + when or if, a key is compromised. + + The recommendation on which KMS to use depends on the environment the application is running + in: + + - For Google Cloud Platform consider [Cloud Key Management](https://cloud.google.com/kms/docs) + - For Amazon Web Services consider [AWS Key Management](https://aws.amazon.com/kms/) + - For on premise or other alternatives to cloud providers, consider [Hashicorp's + Vault](https://www.vaultproject.io/) + - For other cloud providers, please see their documentation + severity: "ERROR" + metadata: + shortDescription: "Missing authentication for critical function (database)" + category: "security" + cwe: "CWE-306" + owasp: + - "A2:2017-Broken Authentication" + - "A07:2021-Identification and Authentication Failures" + - "A07:2025-Authentication Failures" + technology: + - "java" + security-severity: "Critical" \ No newline at end of file diff --git a/data/rules/gitlab-sast-rules/java/xml/rule-XmlDecoder.yml b/data/rules/gitlab-sast-rules/java/xml/rule-XmlDecoder.yml new file mode 100644 index 0000000..9df19bb --- /dev/null +++ b/data/rules/gitlab-sast-rules/java/xml/rule-XmlDecoder.yml @@ -0,0 +1,125 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "java_xml_rule-XmlDecoder" + languages: + - "java" + message: | + Deserialization attacks exploit the process of reading serialized data and turning it back + into an object. By constructing malicious objects and serializing them, an adversary may + attempt to: + + - Inject code that is executed upon object construction, which occurs during the + deserialization process. + - Exploit mass assignment by including fields that are not normally a part of the serialized + data but are read in during deserialization. + + Consider safer alternatives such as serializing data in the JSON format. Ensure any format + chosen allows + the application to specify exactly which object types are allowed to be deserialized. + Additionally, when + deserializing, never deserialize to base object types like `Object` and only cast to the exact + object + type that is expected. + + To protect against mass assignment, only allow deserialization of the specific fields that are + required. If this is not easily done, consider creating an intermediary type that + can be serialized with only the necessary fields exposed. + + Do note that `XMLEncoder` and `XMLDecoder` are not recommended. If the application must + use this serialization method, use a custom ClassLoader to prevent loading of arbitrary + classes: + ``` + XMLDecoder decoder = new XMLDecoder(inputStream, null, null, new ClassLoader() { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (!name.equals(NameOfBeanHere.class.getName()) && + !name.equals(XMLDecoder.class.getName())) { + throw new RuntimeException("Unauthorized deserialization attempt: " + name); + } + + return super.loadClass(name, resolve); + } + }); + ``` + + For more information on XML security see OWASP's guide: + https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#java + + For more details on deserialization attacks in general, see OWASP's guide: + https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html + + It should be noted that [tools exist](https://github.com/frohoff/ysoserial) to + automatically create + exploit code for these vulnerabilities. + metadata: + shortDescription: "Deserialization of untrusted data" + category: "security" + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + security-severity: "High" + patterns: + - pattern: | + (java.beans.XMLDecoder $D).readObject(); + - pattern-not: + pattern-either: + - patterns: + - pattern-inside: | + java.beans.XMLDecoder $DEC = new java.beans.XMLDecoder(..., $CL); + ... + - pattern: $DEC.readObject(); + - metavariable-pattern: + metavariable: $CL + patterns: + - pattern: | + new ClassLoader(){ + ... + $RET loadClass(String name, boolean resolve){ + if($X){ + throw ... + } + ... + } + ... + } + - metavariable-pattern: + metavariable: $X + pattern-either: + - pattern: | + !name.equals(...) + - pattern: | + !$LIST.contains(name) + - patterns: + - pattern-inside: | + ClassLoader $CLASS_LOADER = $CL; + ... + java.beans.XMLDecoder $DEC = new java.beans.XMLDecoder(..., $CLASS_LOADER); + ... + - pattern: $DEC.readObject(); + - metavariable-pattern: + metavariable: $CL + patterns: + - pattern: | + new ClassLoader(){ + ... + $RET loadClass(String name, boolean resolve){ + if($X){ + throw ... + } + ... + } + ... + } + - metavariable-pattern: + metavariable: $X + pattern-either: + - pattern: | + !name.equals(...) + - pattern: | + !$LIST.contains(name) + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/javascript/buf/rule-buffer-noassert-read.yml b/data/rules/gitlab-sast-rules/javascript/buf/rule-buffer-noassert-read.yml new file mode 100644 index 0000000..faf9abe --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/buf/rule-buffer-noassert-read.yml @@ -0,0 +1,69 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: javascript_buf_rule-buffer-noassert-read + patterns: + - pattern: "$OBJ.$METHOD(..., true)" + - metavariable-regex: + metavariable: $METHOD + regex: ^(readUInt8|readUInt16LE|readUInt16BE|readUInt32LE|readUInt32BE|readUInt32BE|readInt8|readInt16LE|readInt16BE|readInt32LE|readInt32BE|readFloatLE|readFloatBE|readDoubleLE|readDoubleBE)$ + severity: "WARNING" + languages: + - "javascript" + metadata: + owasp: + - "A6:2017-Security Misconfiguration" + - "A05:2021-Security Misconfiguration" + cwe: "CWE-125" + shortDescription: "Out-of-bounds read" + security-severity: "MEDIUM" + category: "security" + source-rule-url: "https://github.com/nodesecurity/eslint-plugin-security/blob/master/rules/detect-buffer-noassert.js" + message: | + The application is using Buffer API methods with the `noAssert` + parameter set to `true` for the read buffer methods. This disables + the bounds checking and could result in reading beyond the end of + the buffer, leading to potential memory corruption and security + vulnerabilities. When `noAssert` is set to `true`, the methods + do not perform bounds checking, allowing the offset to be beyond + the end of the buffer. This can lead to out-of-bounds memory access, + causing undefined behavior, crashes, or potential security exploits. + + To mitigate the issue, ensure that the `noAssert` parameter is not + set to `true`. Always use bounds checking to prevent out-of-bounds + memory access. Validate the offset before performing read operations + on the buffer. + + Secure Code Example: + ``` + const buffer = Buffer.alloc(10); // Allocate a buffer of 10 bytes + + function safeBufferMethods(buf) { + const safeOffset = 5; // An offset within the bounds of the buffer + + // Secure read methods + if (safeOffset < buf.length) { + buf.readUInt8(safeOffset); + buf.readUInt16LE(safeOffset); + buf.readUInt16BE(safeOffset); + buf.readUInt32LE(safeOffset); + buf.readUInt32BE(safeOffset); + buf.readInt8(safeOffset); + buf.readInt16LE(safeOffset); + buf.readInt16BE(safeOffset); + buf.readInt32LE(safeOffset); + buf.readInt32BE(safeOffset); + buf.readFloatLE(safeOffset); + buf.readFloatBE(safeOffset); + buf.readDoubleLE(safeOffset); + buf.readDoubleBE(safeOffset); + } + } + try { + safeBufferMethods(buffer); + } catch (error) { + console.error('Caught an error:', error); + } + ``` diff --git a/data/rules/gitlab-sast-rules/javascript/buf/rule-buffer-noassert-write.yml b/data/rules/gitlab-sast-rules/javascript/buf/rule-buffer-noassert-write.yml new file mode 100644 index 0000000..cc941ba --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/buf/rule-buffer-noassert-write.yml @@ -0,0 +1,70 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: javascript_buf_rule-buffer-noassert-write + patterns: + - pattern: "$OBJ.$METHOD(..., true)" + - metavariable-regex: + metavariable: $METHOD + regex: ^(writeUInt8|writeUInt16LE|writeUInt16BE|writeUInt32LE|writeUInt32BE|writeInt8|writeInt16LE|writeInt16BE|writeInt32LE|writeInt32BE|writeFloatLE|writeFloatBE|writeDoubleLE|writeDoubleBE)$ + severity: "WARNING" + languages: + - "javascript" + metadata: + owasp: + - "A6:2017-Security Misconfiguration" + - "A05:2021-Security Misconfiguration" + cwe: "CWE-787" + shortDescription: "Out-of-bounds write" + security-severity: "MEDIUM" + category: "security" + source-rule-url: "https://github.com/nodesecurity/eslint-plugin-security/blob/master/rules/detect-buffer-noassert.js" + message: | + The application is using Buffer API methods with the `noAssert` + parameter set to `true` for the write buffer methods. This disables + the bounds checking and could result in writing beyond the end of + the buffer, leading to potential memory corruption and security + vulnerabilities. When `noAssert` is set to `true`, the methods + do not perform bounds checking, allowing the offset to be beyond + the end of the buffer. This can lead to out-of-bounds memory access, + causing undefined behavior, crashes, or potential security exploits. + + To mitigate the issue, ensure that the `noAssert` parameter is not + set to `true`. Always use bounds checking to prevent out-of-bounds + memory access. Validate the offset before performing write operations + on the buffer. + + Secure Code Example: + ``` + const buffer = Buffer.alloc(10); // Allocate a buffer of 10 bytes + + function safeBufferMethods(buf) { + const safeOffset = 5; // An offset within the bounds of the buffer + + // Secure write methods + if (safeOffset < buf.length) { + buf.writeUInt8(0, safeOffset); + buf.writeUInt16LE(0, safeOffset); + buf.writeUInt16BE(0, safeOffset); + buf.writeUInt32LE(0, safeOffset); + buf.writeUInt32BE(0, safeOffset); + buf.writeInt8(0, safeOffset); + buf.writeInt16LE(0, safeOffset); + buf.writeInt16BE(0, safeOffset); + buf.writeInt32LE(0, safeOffset); + buf.writeInt32BE(0, safeOffset); + buf.writeFloatLE(0, safeOffset); + buf.writeFloatBE(0, safeOffset); + buf.writeDoubleLE(0, safeOffset); + buf.writeDoubleBE(0, safeOffset); + } + } + + try { + safeBufferMethods(buffer); + } catch (error) { + console.error('Caught an error:', error); + } + ``` diff --git a/data/rules/gitlab-sast-rules/javascript/buf/rule-detect-new-buffer.yml b/data/rules/gitlab-sast-rules/javascript/buf/rule-detect-new-buffer.yml new file mode 100644 index 0000000..8c35dc6 --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/buf/rule-detect-new-buffer.yml @@ -0,0 +1,57 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_buf_rule-detect-new-buffer" + languages: + - "javascript" + - "typescript" + patterns: + - patterns: + - pattern-not-inside: |- + require("safe-buffer") + ... + - pattern-not-inside: |- + import { Buffer } from "safe-buffer" + ... + - pattern-inside: |- + function $FUNC(..., $X, ...) { ... } + - pattern: "new Buffer($X,...)" + - pattern-not: | + new Buffer("...",...) + - pattern-not: | + new Buffer([...] , ...) + message: | + The application was found calling the `new Buffer` constructor which has been deprecated + since Node 8. + By passing in a non-literal value, an adversary could allocate large amounts of memory. + + Other issues also exist with the `Buffer` constructor: + - Older versions would return uninitialized memory, which could contain sensitive information + - Unable to easily determine what a Buffer contained if passed a non-literal value + + To remediate this issue, use `Buffer.alloc` or `Buffer.from` instead to allocate a new + `Buffer`. + + Example using `Buffer.alloc` instead of `new Buffer(...)`: + ``` + // Create a new buffer using Buffer.from + const buf = Buffer.from([1, 2, 3, 4]); + // Work with buf + ``` + + For more information on migrating to `Buffer.from()`/`Buffer.alloc()` see: + - https://nodejs.org/en/docs/guides/buffer-constructor-deprecation + metadata: + shortDescription: "Allocation of resources without limits or throttling" + cwe: "CWE-770" + category: "security" + owasp: + - "A9:2017-Using Components with Known Vulnerabilities" + - "A06:2021-Vulnerable and Outdated Components" + # yamllint disable + source-rule-url: "https://github.com/eslint-community/eslint-plugin-security/blob/main/rules/detect-new-buffer.js" + # yamllint enable + security-severity: "Medium" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/javascript/dos/rule-non-literal-regexp.yml b/data/rules/gitlab-sast-rules/javascript/dos/rule-non-literal-regexp.yml new file mode 100644 index 0000000..0f813b7 --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/dos/rule-non-literal-regexp.yml @@ -0,0 +1,73 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_dos_rule-non-literal-regexp" + languages: + - "javascript" + - "typescript" + pattern-either: + - patterns: + - pattern: | + new RegExp(...) + - pattern-not: | + new RegExp("...", ...) + - pattern-not: | + new RegExp(/.../, ...) + - patterns: + - pattern: | + RegExp(...) + - pattern-not: | + RegExp("...", ...) + - pattern-not: | + RegExp(/.../, ...) + - patterns: + - pattern: | + "...".$METHOD(...) + - pattern-not: | + "...".$METHOD("...") + - pattern-not: | + "...".$METHOD(/.../) + - metavariable-regex: + metavariable: $METHOD + regex: "^(match|search)$" + message: | + The `RegExp` constructor was called with a non-literal value. If an adversary were able to + supply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS) + against the application. In Node applications, this could cause the entire application to no + longer be responsive to other users' requests. + + To remediate this issue, never allow user-supplied regular expressions. Instead, the regular + expression should be hardcoded. If this is not possible, consider using an alternative regular + expression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative + that does not support backtracking, which is what leads to ReDoS. + + Example using re2 which does not support backtracking (Note: it is still recommended to + never use user-supplied input): + ``` + // Import the re2 module + const RE2 = require('re2'); + + function match(userSuppliedRegex, userInput) { + // Create a RE2 object with the user supplied regex, this is relatively safe + // due to RE2 not supporting backtracking which can be abused to cause long running + // queries + var re = new RE2(userSuppliedRegex); + // Execute the regular expression against some userInput + var result = re.exec(userInput); + // Work with the result + } + ``` + + For more information on Regular Expression DoS see: + - https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS + metadata: + cwe: "CWE-185" + shortDescription: "Regular expression with non-literal value" + category: "security" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + security-severity: "Medium" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/javascript/eval/rule-eval-with-expression.yml b/data/rules/gitlab-sast-rules/javascript/eval/rule-eval-with-expression.yml new file mode 100644 index 0000000..f4df539 --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/eval/rule-eval-with-expression.yml @@ -0,0 +1,143 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_eval_rule-eval-with-expression" + languages: + - "javascript" + - "typescript" + patterns: + - pattern-either: + - patterns: + - pattern: | + eval($OBJ) + - pattern-not: | + eval("...") + - patterns: + - pattern: | + $A.eval($OBJ) + - pattern-not: | + $A.eval("...") + - patterns: + - pattern: | + (..., eval)($OBJ) + - pattern-not: | + (..., eval)("...") + - patterns: + - pattern: | + $ALIAS = eval; + ... + $ALIAS($OBJ) + - pattern-not: | + $ALIAS = eval; + ... + $ALIAS("...") + - pattern: | + new Function(..., "..." + $OBJ + "...") + - pattern: | + $BODY = "..." + $OBJ + "..." + ... + new Function(..., $BODY) + - pattern: | + new Function(..., `...${...}...`) + - pattern: | + $BODY = `...${...}...` + ... + new Function(..., $BODY) + - pattern: | + Function(..., "..." + $OBJ + "...") + - pattern: | + $BODY = "..." + $OBJ + "..." + ... + Function(..., $BODY) + - pattern: | + $BODY = `...${...}...` + ... + Function(..., $BODY) + - pattern: | + Function(..., `...${...}...`) + - pattern: | + setTimeout("..." + $OBJ + "...", ...) + - pattern: | + setTimeout(`...${...}...`, ...) + - pattern: | + setInterval("..." + $OBJ + "...", ...) + - pattern: | + setInterval(`...${...}...`, ...) + - pattern-not: | + setTimeout("...", ...) + - pattern-not: | + setInterval("...", ...) + severity: "WARNING" + message: | + The application was found calling the `eval` function OR Function() + constructor OR setTimeout() OR setInterval() methods. If the + + variables or strings or functions passed to these methods contains user-supplied input, an adversary could attempt to execute arbitrary + + JavaScript + + code. This could lead to a full system compromise in Node applications or Cross-site Scripting + + (XSS) in web applications. + + + To remediate this issue, remove all calls to above methods and consider alternative methods for + + executing + + the necessary business logic. There is almost no safe method of calling `eval` or other above stated sinks with + + user-supplied input. + + Instead, consider alternative methods such as using property accessors to dynamically access + + values. + + + Example using property accessors to dynamically access an object's property: + + ``` + + // Define an object + + const obj = {key1: 'value1', key2: 'value2'}; + + // Get key dynamically from user input + + const key = getUserInput(); + + // Check if the key exists in our object and return it, or a default empty string + + const value = (obj.hasOwnProperty(key)) ? obj[key] : ''; + + // Work with the value + + ``` + + + For more information on why not to use `eval`, and alternatives see: + + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval! + + Other References: + + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function + + - https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + + - https://developer.mozilla.org/en-US/docs/Web/API/setInterval + metadata: + cwe: "CWE-95" + shortDescription: "Improper neutralization of directives in dynamically evaluated + code ('Eval Injection')" + category: "security" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + # yamllint disable + source-rule-url: "https://github.com/nodesecurity/eslint-plugin-security/blob/master/rules/detect-eval-with-expression.js" + # yamllint enable + security-severity: "High" diff --git a/data/rules/gitlab-sast-rules/javascript/pathtraversal/rule-non-literal-fs-filename.yml b/data/rules/gitlab-sast-rules/javascript/pathtraversal/rule-non-literal-fs-filename.yml new file mode 100644 index 0000000..c0d8dcf --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/pathtraversal/rule-non-literal-fs-filename.yml @@ -0,0 +1,163 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_pathtraversal_rule-non-literal-fs-filename" + languages: + - "typescript" + - "javascript" + patterns: + - pattern-either: + - pattern-inside: | + import $MOD from $IMP + ... + - pattern-inside: | + $MOD = require($IMP) + ... + - metavariable-comparison: + metavariable: "$IMP" + comparison: "$IMP in ['fs', 'node:fs', 'fs/promises', 'node:fs/promises', 'fs-extra']" + - pattern-not: "$MOD.appendFile(\"...\", ...)" + - pattern-not: "$MOD.appendFileSync(\"...\", ...)" + - pattern-not: "$MOD.chmod(\"...\", ...)" + - pattern-not: "$MOD.chmodSync(\"...\", ...)" + - pattern-not: "$MOD.chown(\"...\", ...)" + - pattern-not: "$MOD.chownSync(\"...\", ...)" + - pattern-not: "$MOD.createReadStream(\"...\", ...)" + - pattern-not: "$MOD.createWriteStream(\"...\", ...)" + - pattern-not: "$MOD.exists(\"...\", ...)" + - pattern-not: "$MOD.existsSync(\"...\", ...)" + - pattern-not: "$MOD.lchmod(\"...\", ...)" + - pattern-not: "$MOD.lchmodSync(\"...\", ...)" + - pattern-not: "$MOD.lchown(\"...\", ...)" + - pattern-not: "$MOD.lchownSync(\"...\", ...)" + - pattern-not: "$MOD.link(\"...\", \"...\", ...)" + - pattern-not: "$MOD.linkSync(\"...\", \"...\", ...)" + - pattern-not: "$MOD.lstat(\"...\", ...)" + - pattern-not: "$MOD.lstatSync(\"...\", ...)" + - pattern-not: "$MOD.mkdir(\"...\", ...)" + - pattern-not: "$MOD.mkdirSync(\"...\", ...)" + - pattern-not: "$MOD.open(\"...\", ...)" + - pattern-not: "$MOD.openSync(\"...\", ...)" + - pattern-not: "$MOD.readdir(\"...\", ...)" + - pattern-not: "$MOD.readdirSync(\"...\", ...)" + - pattern-not: "$MOD.readFile(\"...\", ...)" + - pattern-not: "$MOD.readFileSync(\"...\", ...)" + - pattern-not: "$MOD.readlink(\"...\", ...)" + - pattern-not: "$MOD.readlinkSync(\"...\", ...)" + - pattern-not: "$MOD.realpath(\"...\", ...)" + - pattern-not: "$MOD.realpathSync(\"...\", ...)" + - pattern-not: "$MOD.rename(\"...\", \"...\", ...)" + - pattern-not: "$MOD.renameSync(\"...\", \"...\", ...)" + - pattern-not: "$MOD.rmdir(\"...\", ...)" + - pattern-not: "$MOD.rmdirSync(\"...\", ...)" + - pattern-not: "$MOD.stat(\"...\", ...)" + - pattern-not: "$MOD.statSync(\"...\", ...)" + - pattern-not: "$MOD.symlink(\"...\", \"...\", ...)" + - pattern-not: "$MOD.symlinkSync(\"...\", \"...\", ...)" + - pattern-not: "$MOD.truncate(\"...\", ...)" + - pattern-not: "$MOD.truncateSync(\"...\", ...)" + - pattern-not: "$MOD.unlink(\"...\", ...)" + - pattern-not: "$MOD.unlinkSync(\"...\", ...)" + - pattern-not: "$MOD.unwatchFile(\"...\", ...)" + - pattern-not: "$MOD.utimes(\"...\", ...)" + - pattern-not: "$MOD.utimesSync(\"...\", ...)" + - pattern-not: "$MOD.watch(\"...\", ...)" + - pattern-not: "$MOD.watchFile(\"...\", ...)" + - pattern-not: "$MOD.writeFile(\"...\", ...)" + - pattern-not: "$MOD.writeFileSync(\"...\", ...)" + - pattern-either: + - pattern: "$MOD.appendFile(...)" + - pattern: "$MOD.appendFileSync(...)" + - pattern: "$MOD.chmod(...)" + - pattern: "$MOD.chmodSync(...)" + - pattern: "$MOD.chown(...)" + - pattern: "$MOD.chownSync(...)" + - pattern: "$MOD.createReadStream(...)" + - pattern: "$MOD.createWriteStream(...)" + - pattern: "$MOD.exists(...)" + - pattern: "$MOD.existsSync(...)" + - pattern: "$MOD.lchmod(...)" + - pattern: "$MOD.lchmodSync(...)" + - pattern: "$MOD.lchown(...)" + - pattern: "$MOD.lchownSync(...)" + - pattern: "$MOD.link(...)" + - pattern: "$MOD.linkSync(...)" + - pattern: "$MOD.lstat(...)" + - pattern: "$MOD.lstatSync(...)" + - pattern: "$MOD.mkdir(...)" + - pattern: "$MOD.mkdirSync(...)" + - pattern: "$MOD.open(...)" + - pattern: "$MOD.openSync(...)" + - pattern: "$MOD.readdir(...)" + - pattern: "$MOD.readdirSync(...)" + - pattern: "$MOD.readFile(...)" + - pattern: "$MOD.readFileSync(...)" + - pattern: "$MOD.readlink(...)" + - pattern: "$MOD.readlinkSync(...)" + - pattern: "$MOD.realpath(...)" + - pattern: "$MOD.realpathSync(...)" + - pattern: "$MOD.rename(...)" + - pattern: "$MOD.renameSync(...)" + - pattern: "$MOD.rmdir(...)" + - pattern: "$MOD.rmdirSync(...)" + - pattern: "$MOD.stat(...)" + - pattern: "$MOD.statSync(...)" + - pattern: "$MOD.symlink(...)" + - pattern: "$MOD.symlinkSync(...)" + - pattern: "$MOD.truncate(...)" + - pattern: "$MOD.truncateSync(...)" + - pattern: "$MOD.unlink(...)" + - pattern: "$MOD.unlinkSync(...)" + - pattern: "$MOD.unwatchFile(...)" + - pattern: "$MOD.utimes(...)" + - pattern: "$MOD.utimesSync(...)" + - pattern: "$MOD.watch(...)" + - pattern: "$MOD.watchFile(...)" + - pattern: "$MOD.writeFile(...)" + - pattern: "$MOD.writeFileSync(...)" + message: | + The application dynamically constructs file or path information. If the path + information comes from user-supplied input, it could be abused to read sensitive files, + access other users' data, or aid in exploitation to gain further system access. + + User input should never be used in constructing paths or files for interacting + with the filesystem. This includes filenames supplied by user uploads or downloads. + If possible, consider hashing user input or using unique values and + use `path.normalize` to resolve and validate the path information + prior to processing any file functionality. + + Example using `path.normalize` and not allowing direct user input: + ``` + // User input, saved only as a reference + // id is a randomly generated UUID to be used as the filename + const userData = {userFilename: userSuppliedFilename, id: crypto.randomUUID()}; + // Restrict all file processing to this directory only + const basePath = '/app/restricted/'; + + // Create the full path, but only use our random generated id as the filename + const joinedPath = path.join(basePath, userData.id); + // Normalize path, removing any '..' + const fullPath = path.normalize(joinedPath); + // Verify the fullPath is contained within our basePath + if (!fullPath.startsWith(basePath)) { + console.log("Invalid path specified!"); + } + // Process / work with file + // ... + ``` + + For more information on path traversal issues see OWASP: + https://owasp.org/www-community/attacks/Path_Traversal + metadata: + shortDescription: "Improper limitation of a pathname to a restricted directory + ('Path Traversal')" + cwe: "CWE-22" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/javascript/random/rule-pseudo-random-bytes.yml b/data/rules/gitlab-sast-rules/javascript/random/rule-pseudo-random-bytes.yml new file mode 100644 index 0000000..a984bf2 --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/random/rule-pseudo-random-bytes.yml @@ -0,0 +1,38 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_random_rule-pseudo-random-bytes" + languages: + - "javascript" + - "typescript" + pattern: "crypto.pseudoRandomBytes" + severity: "WARNING" + message: | + Depending on the context, generating weak random numbers may expose cryptographic functions, + which rely on these numbers, to be exploitable. When generating numbers for sensitive values + such as tokens, nonces, and cryptographic keys, it is recommended that the `randomBytes` method + of the `crypto` module be used instead of `pseudoRandomBytes`. + + Example using `randomBytes`: + ``` + // Generate 256 bytes of random data + const randomBytes = crypto.randomBytes(256); + ``` + + For more information on JavaScript Cryptography see: + https://nodejs.org/api/crypto.html#cryptorandombytessize-callback + metadata: + cwe: "CWE-338" + shortDescription: "Use of cryptographically weak pseudo-random number generator + (PRNG)" + category: "security" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + # yamllint disable + source-rule-url: "https://github.com/nodesecurity/eslint-plugin-security/blob/master/rules/detect-pseudoRandomBytes.js" + # yamllint enable + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/javascript/react/rule-dangerouslysetinnerhtml.yml b/data/rules/gitlab-sast-rules/javascript/react/rule-dangerouslysetinnerhtml.yml new file mode 100644 index 0000000..dfbbafa --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/react/rule-dangerouslysetinnerhtml.yml @@ -0,0 +1,64 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_react_rule-dangerouslysetinnerhtml" + languages: + - "typescript" + - "javascript" + pattern-either: + - pattern: | + <$X dangerouslySetInnerHTML=... /> + - pattern: | + {dangerouslySetInnerHTML: ...} + severity: "WARNING" + message: | + The application was found calling `dangerouslySetInnerHTML` which may lead to Cross Site + Scripting (XSS). By default, React components will encode the data properly before rendering. + Calling `dangerouslySetInnerHTML` disables this encoding and allows raw markup + and JavaScript to be executed. + + XSS is an attack which exploits a web application or system to treat + user input as markup or script code. It is important to encode the data, depending on the + specific context it is used in. There are at least six context types: + + - Inside HTML tags `
context 1
` + - Inside attributes: `
` + - Inside event attributes `` + - Inside script blocks: `` + - Unsafe element HTML assignment: `element.innerHTML = "context 5"` + - Inside URLs: `link` + + Script blocks alone have multiple ways they need to be encoded. Extra care must be taken if + user input + is ever output inside of script tags. + + User input that is displayed within the application must be encoded, sanitized or validated + to ensure it cannot be treated as HTML or executed as Javascript code. Care must also be + taken + to not mix server-side templating with client-side templating, as the server-side templating + will + not encode things like {{ 7*7 }} which may execute client-side templating features. + + It is _NOT_ advised to encode user input prior to inserting into a data store. The data will + need to be + encoded depending on context of where it is output. It is much safer to force the displaying + system to + handle the encoding and not attempt to guess how it should be encoded. + + Remove the call to `dangerouslySetInnerHTML` or ensure that the data used in this call does + not come from user-supplied input. + + For more information on dangerously setting inner HTML see: + - https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html + metadata: + cwe: "CWE-79" + owasp: + - "A7:2017-Cross-Site Scripting (XSS)" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of input during web page generation + ('Cross-site Scripting')" + category: "security" + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/javascript/require/rule-non-literal-require.yml b/data/rules/gitlab-sast-rules/javascript/require/rule-non-literal-require.yml new file mode 100644 index 0000000..b81110b --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/require/rule-non-literal-require.yml @@ -0,0 +1,34 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_require_rule-non-literal-require" + languages: + - "javascript" + - "typescript" + patterns: + - pattern: "require($OBJ)" + - pattern-not: "require('...')" + severity: "WARNING" + message: | + The application was found to dynamically import a module by calling `require` using a + non-literal string. An adversary might be able to read the first line of + arbitrary files. If they had write access to the file system, they may also be able to + execute arbitrary code. + + To remediate this issue, use a hardcoded string literal when calling `require`. Never call it + it with dynamically created variables or user-supplied data. + metadata: + cwe: "CWE-95" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of directives in dynamically evaluated + code ('Eval Injection')" + category: "security" + # yamllint disable + source-rule-url: "https://github.com/nodesecurity/eslint-plugin-security/blob/master/rules/detect-non-literal-require.js" + # yamllint enable + security-severity: "Low" diff --git a/data/rules/gitlab-sast-rules/javascript/timing/rule-possible-timing-attacks.yml b/data/rules/gitlab-sast-rules/javascript/timing/rule-possible-timing-attacks.yml new file mode 100644 index 0000000..4affb2f --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/timing/rule-possible-timing-attacks.yml @@ -0,0 +1,527 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_timing_rule-possible-timing-attacks" + languages: + - "javascript" + patterns: + - pattern-not: "if ($Z == null) { ... };" + - pattern-not: "if ($Z === null) { ... };" + - pattern-not: "if ($Z != null) { ... };" + - pattern-not: "if ($Z !== null) { ... };" + - pattern-not: "if ($Q != undefined) { ... };" + - pattern-not: "if ($Q !== undefined) { ... };" + - pattern-not: "if ($Q == undefined) { ... };" + - pattern-not: "if ($Q === undefined) { ... };" + - pattern-not: "return $Y == null;" + - pattern-not: "return $Y === null;" + - pattern-not: "return $Y != null;" + - pattern-not: "return $Y !== null;" + - pattern-not: "return $Y == undefined;" + - pattern-not: "return $Y === undefined;" + - pattern-not: "return $Y != undefined;" + - pattern-not: "return $Y !== undefined;" + - pattern-either: + - pattern: | + if (password == $X) { + ... + } + - pattern: | + if ($X == password) { + ... + } + - pattern: | + if (password === $X) { + ... + } + - pattern: | + if ($X === password) { + ... + } + - pattern: | + if (pass == $X) { + ... + } + - pattern: | + if ($X == pass) { + ... + } + - pattern: | + if (pass === $X) { + ... + } + - pattern: | + if ($X === pass) { + ... + } + - pattern: | + if (secret == $X) { + ... + } + - pattern: | + if ($X == secret) { + ... + } + - pattern: | + if (secret === $X) { + ... + } + - pattern: | + if ($X === secret) { + ... + } + - pattern: | + if (api == $X) { + ... + } + - pattern: | + if ($X == api) { + ... + } + - pattern: | + if (api === $X) { + ... + } + - pattern: | + if ($X === api) { + ... + } + - pattern: | + if (apiKey == $X) { + ... + } + - pattern: | + if ($X == apiKey) { + ... + } + - pattern: | + if (apiKey === $X) { + ... + } + - pattern: | + if ($X === apiKey) { + ... + } + - pattern: | + if (apiSecret == $X) { + ... + } + - pattern: | + if ($X == apiSecret) { + ... + } + - pattern: | + if (apiSecret === $X) { + ... + } + - pattern: | + if ($X === apiSecret) { + ... + } + - pattern: | + if (token == $X) { + ... + } + - pattern: | + if ($X == token) { + ... + } + - pattern: | + if (token === $X) { + ... + } + - pattern: | + if ($X === token) { + ... + } + - pattern: | + if (hash == $X) { + ... + } + - pattern: | + if ($X == hash) { + ... + } + - pattern: | + if (hash === $X) { + ... + } + - pattern: | + if ($X === hash) { + ... + } + - pattern: | + if (auth_token == $X) { + ... + } + - pattern: | + if ($X == auth_token) { + ... + } + - pattern: | + if (auth_token === $X) { + ... + } + - pattern: | + if ($X === auth_token) { + ... + } + - pattern: | + if (password != $X) { + ... + } + - pattern: | + if ($X != password) { + ... + } + - pattern: | + if (password !== $X) { + ... + } + - pattern: | + if ($X !== password) { + ... + } + - pattern: | + if (pass != $X) { + ... + } + - pattern: | + if ($X != pass) { + ... + } + - pattern: | + if (pass !== $X) { + ... + } + - pattern: | + if ($X !== pass) { + ... + } + - pattern: | + if (secret != $X) { + ... + } + - pattern: | + if ($X != secret) { + ... + } + - pattern: | + if (secret !== $X) { + ... + } + - pattern: | + if ($X !== secret) { + ... + } + - pattern: | + if (api != $X) { + ... + } + - pattern: | + if ($X != api) { + ... + } + - pattern: | + if (api !== $X) { + ... + } + - pattern: | + if ($X !== api) { + ... + } + - pattern: | + if (apiKey != $X) { + ... + } + - pattern: | + if ($X != apiKey) { + ... + } + - pattern: | + if (apiKey !== $X) { + ... + } + - pattern: | + if ($X !== apiKey) { + ... + } + - pattern: | + if (apiSecret != $X) { + ... + } + - pattern: | + if ($X != apiSecret) { + ... + } + - pattern: | + if (apiSecret !== $X) { + ... + } + - pattern: | + if ($X !== apiSecret) { + ... + } + - pattern: | + if (token != $X) { + ... + } + - pattern: | + if ($X != token) { + ... + } + - pattern: | + if (token !== $X) { + ... + } + - pattern: | + if ($X !== token) { + ... + } + - pattern: | + if (hash != $X) { + ... + } + - pattern: | + if ($X != hash) { + ... + } + - pattern: | + if (hash !== $X) { + ... + } + - pattern: | + if ($X !== hash) { + ... + } + - pattern: | + if (auth_token != $X) { + ... + } + - pattern: | + if ($X != auth_token) { + ... + } + - pattern: | + if (auth_token !== $X) { + ... + } + - pattern: | + if ($X !== auth_token) { + ... + } + - pattern: | + return $X === auth_token; + - pattern: | + return auth_token === $X; + - pattern: | + return $X === token; + - pattern: | + return token === $X; + - pattern: | + return $X === hash; + - pattern: | + return hash === $X; + - pattern: | + return $X === password; + - pattern: | + return password === $X; + - pattern: | + return $X === pass; + - pattern: | + return pass === $X; + - pattern: | + return $X === apiKey; + - pattern: | + return apiKey === $X; + - pattern: | + return $X === apiSecret; + - pattern: | + return apiSecret === $X; + - pattern: | + return $X === api_key; + - pattern: | + return api_key === $X; + - pattern: | + return $X === api_secret; + - pattern: | + return api_secret === $X; + - pattern: | + return $X === secret; + - pattern: | + return secret === $X; + - pattern: | + return $X === api; + - pattern: | + return api === $X; + - pattern: | + return $X == auth_token; + - pattern: | + return auth_token == $X; + - pattern: | + return $X == token; + - pattern: | + return token == $X; + - pattern: | + return $X == hash; + - pattern: | + return hash == $X; + - pattern: | + return $X == password; + - pattern: | + return password == $X; + - pattern: | + return $X == pass; + - pattern: | + return pass == $X; + - pattern: | + return $X == apiKey; + - pattern: | + return apiKey == $X; + - pattern: | + return $X == apiSecret; + - pattern: | + return apiSecret == $X; + - pattern: | + return $X == api_key; + - pattern: | + return api_key == $X; + - pattern: | + return $X == api_secret; + - pattern: | + return api_secret == $X; + - pattern: | + return $X == secret; + - pattern: | + return secret == $X; + - pattern: | + return $X == api; + - pattern: | + return api == $X; + - pattern: | + return $X !== auth_token; + - pattern: | + return auth_token !== $X; + - pattern: | + return $X !== token; + - pattern: | + return token !== $X; + - pattern: | + return $X !== hash; + - pattern: | + return hash !== $X; + - pattern: | + return $X !== password; + - pattern: | + return password !== $X; + - pattern: | + return $X !== pass; + - pattern: | + return pass !== $X; + - pattern: | + return $X !== apiKey; + - pattern: | + return apiKey !== $X; + - pattern: | + return $X !== apiSecret; + - pattern: | + return apiSecret !== $X; + - pattern: | + return $X !== api_key; + - pattern: | + return api_key !== $X; + - pattern: | + return $X !== api_secret; + - pattern: | + return api_secret !== $X; + - pattern: | + return $X !== secret; + - pattern: | + return secret !== $X; + - pattern: | + return $X !== api; + - pattern: | + return api !== $X; + - pattern: | + return $X != auth_token; + - pattern: | + return auth_token != $X; + - pattern: | + return $X != token; + - pattern: | + return token != $X; + - pattern: | + return $X != hash; + - pattern: | + return hash != $X; + - pattern: | + return $X != password; + - pattern: | + return password != $X; + - pattern: | + return $X != pass; + - pattern: | + return pass != $X; + - pattern: | + return $X != apiKey; + - pattern: | + return apiKey != $X; + - pattern: | + return $X != apiSecret; + - pattern: | + return apiSecret != $X; + - pattern: | + return $X != api_key; + - pattern: | + return api_key != $X; + - pattern: | + return $X != api_secret; + - pattern: | + return api_secret != $X; + - pattern: | + return $X != secret; + - pattern: | + return secret != $X; + - pattern: | + return $X != api; + - pattern: | + return api != $X; + severity: "WARNING" + message: | + The application was found executing string comparisons using one of `===`, `!==`, `==` or `!=` + against security sensitive values. String comparisons like this are not constant time, meaning + the + first character found not to match in the two strings will immediately exit the conditional + statement. + This allows an adversary to calculate or observe small timing differences depending on the + strings + passed to this comparison. This potentially allows an adversary the ability to brute force a + string + that will match the expected value by monitoring different character values. + + To remediate this issue, use the `crypto.timingSafeEqual` method when comparing strings. + + Example using `crypto.timingSafeEqual` to safely compare strings: + ``` + function constantTimeIsPasswordEqual(userInput) { + // Retrieve the password from a secure data store such as a KMS or Hashicorp's vault. + const password = getPasswordFromSecureDataStore(); + // Use crypto timingSafeEqual to ensure the comparison is done in constant time. + return crypto.timingSafeEqual(Buffer.from(userInput, 'utf-8'), Buffer.from(password, + 'utf-8')); + } + ``` + + For more information on constant time comparison see: + - https://nodejs.org/api/crypto.html#crypto_crypto_timingsafeequal_a_b + metadata: + cwe: "CWE-208" + shortDescription: "Observable timing discrepancy" + category: "security" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/javascript/xss/rule-mustache-escape.yml b/data/rules/gitlab-sast-rules/javascript/xss/rule-mustache-escape.yml new file mode 100644 index 0000000..429504f --- /dev/null +++ b/data/rules/gitlab-sast-rules/javascript/xss/rule-mustache-escape.yml @@ -0,0 +1,44 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "javascript_xss_rule-mustache-escape" + languages: + - "javascript" + - "typescript" + patterns: + - pattern-inside: | + $OBJ = require('mustache') + ... + - pattern-either: + - pattern: | + $OBJ.escape = function($TEXT) { + ... + return $TEXT; + } + - patterns: + - metavariable-regex: + metavariable: "$X" + regex: "\"\\{\\{\\{(.+?)\\}\\}\\}\"" + - pattern: "$OBJ.render($X, ... )" + - patterns: + - metavariable-regex: + metavariable: "$Y" + regex: "\"\\{\\{\\&(.+?)\\}\\}\"" + - pattern: "$OBJ.render($Y, ... )" + severity: "WARNING" + message: | + Markup escaping disabled. This can be used with some template engines to escape + disabling of HTML entities, which can lead to XSS attacks. + metadata: + cwe: "CWE-79" + owasp: + - "A7:2017-Cross-Site Scripting (XSS)" + - "A03:2021-Injection" + - "A05:2025-Injection" + category: "security" + shortDescription: "Improper neutralization of input during web page generation + (XSS)" + # yamllint enable + security-severity: "Medium" diff --git a/data/rules/gitlab-sast-rules/python/bind_all_interfaces/rule-general-bindall-interfaces.yml b/data/rules/gitlab-sast-rules/python/bind_all_interfaces/rule-general-bindall-interfaces.yml new file mode 100644 index 0000000..b642e26 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/bind_all_interfaces/rule-general-bindall-interfaces.yml @@ -0,0 +1,57 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_bind-all-interfaces_rule-general-bindall-interfaces" + languages: + - "python" + message: | + Binding to all network interfaces can potentially open up a service to + traffic on unintended interfaces, that may not be properly documented or + secured. By passing "0.0.0.0", "::" or an empty string as the address to the `socket.bind` + function, + the application will bind to all interfaces. + + Consider passing in the interface ip address through an environment variable, + configuration file, or by determining the primary interface(s) IP address. + + Example getting the IP address from an environment variable `IP_ADDRESS`: + ``` + # Get the IP_ADDRESS env variable, or bind to + # 127.0.0.1 if it is not set + address = os.getenv("IP_ADDRESS", "127.0.0.1") + # Create an internet socket + sock = socket.socket(socket.AF_INET) + # Set the port to listen on + port = 9777 + # Bind to the address and port combination + sock.bind((address, port)) + # Listen for connections + sock.listen() + # Handle the connection + ``` + patterns: + - pattern-either: + - pattern: | + $S = socket.socket(...) + ... + $S.bind(("0.0.0.0", ...)) + - pattern: | + $S = socket.socket(...) + ... + $S.bind(("::", ...)) + - pattern: | + $S = socket.socket(...) + ... + $S.bind(("", ...)) + metadata: + category: "security" + owasp: + - "A6:2017-Security Misconfiguration" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + shortDescription: "Binding to an unrestricted IP address" + cwe: "CWE-1327" + security-severity: "Low" + severity: "INFO" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-cipher-modes.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-cipher-modes.yml new file mode 100644 index 0000000..f34f74b --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-cipher-modes.yml @@ -0,0 +1,79 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-cipher-modes" + languages: + - "python" + message: | + Cryptographic algorithms provide many different modes of operation, only some of which provide + message integrity. Without message integrity it could be possible for an adversary to attempt + to tamper with the ciphertext which could lead to compromising the encryption key. Newer + algorithms + apply message integrity to validate ciphertext has not been tampered with. + + Instead of using an algorithm that requires configuring a cipher mode, an algorithm + that has built-in message integrity should be used. Consider using `ChaCha20Poly1305` or + `AES-256-GCM` instead. + + For older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is + recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + pattern: "cryptography.hazmat.primitives.ciphers.modes.ECB(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-blowfish.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-blowfish.yml new file mode 100644 index 0000000..0bddb0f --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-blowfish.yml @@ -0,0 +1,82 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-cipher-blowfish" + languages: + - "python" + message: | + The Blowfish encryption algorithm was meant as a drop-in replacement for DES and was created in + 1993. Smaller key sizes may make the ciphertext vulnerable to [birthday + attacks](https://en.wikipedia.org/wiki/Birthday_attack). While no known attacks against + Blowfish + exist, it should never be used to encrypt files over 4GB in size. If possible consider + using ChaCha20Poly1305 or AES-GCM instead of Blowfish. + + For older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is + recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Cryptodome.Cipher.Blowfish.new(...)" + - pattern: "Crypto.Cipher.Blowfish.new(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-des.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-des.yml new file mode 100644 index 0000000..fe42b44 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-des.yml @@ -0,0 +1,80 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-cipher-des" + languages: + - "python" + message: | + DES, TripleDES, RC2 and RC4 are all considered broken or insecure cryptographic algorithms. + Newer algorithms apply message integrity to validate ciphertext has not been tampered + with. Consider using `ChaCha20Poly1305` instead as it is easier and faster than the + alternatives such as `AES-256-GCM`. + + For older applications that don't have support for `ChaCha20Poly1305`, + `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Cryptodome.Cipher.DES.new(...)" + - pattern: "Crypto.Cipher.DES.new(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-rc2.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-rc2.yml new file mode 100644 index 0000000..c7929b3 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-rc2.yml @@ -0,0 +1,80 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-cipher-rc2" + languages: + - "python" + message: | + DES, TripleDES, RC2 and RC4 are all considered broken or insecure cryptographic algorithms. + Newer algorithms apply message integrity to validate ciphertext has not been tampered + with. Consider using `ChaCha20Poly1305` instead as it is easier and faster than the + alternatives such as `AES-256-GCM`. + + For older applications that don't have support for `ChaCha20Poly1305`, + `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Cryptodome.Cipher.ARC2.new(...)" + - pattern: "Crypto.Cipher.ARC2.new" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-rc4.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-rc4.yml new file mode 100644 index 0000000..1ca7819 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-rc4.yml @@ -0,0 +1,80 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-cipher-rc4" + languages: + - "python" + message: | + DES, TripleDES, RC2 and RC4 are all considered broken or insecure cryptographic algorithms. + Newer algorithms apply message integrity to validate ciphertext has not been tampered + with. Consider using `ChaCha20Poly1305` instead as it is easier and faster than the + alternatives such as `AES-256-GCM`. + + For older applications that don't have support for `ChaCha20Poly1305`, + `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Cryptodome.Cipher.ARC4.new(...)" + - pattern: "Crypto.Cipher.ARC4.new(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-xor.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-xor.yml new file mode 100644 index 0000000..0b146e8 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-cipher-xor.yml @@ -0,0 +1,80 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-cipher-xor" + languages: + - "python" + message: | + The application was found using the `xor` algorithm, which can be trivially decoded. + Newer algorithms apply message integrity to validate ciphertext has not been tampered + with. Consider using `ChaCha20Poly1305` instead as it is easier and faster than the + alternatives such as `AES-256-GCM`. + + For older applications that don't have support for `ChaCha20Poly1305`, + `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Cryptodome.Cipher.XOR.new(...)" + - pattern: "Crypto.Cipher.XOR.new(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-encrypt-dsa-rsa.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-encrypt-dsa-rsa.yml new file mode 100644 index 0000000..f180f44 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-encrypt-dsa-rsa.yml @@ -0,0 +1,80 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-encrypt-dsa-rsa" + languages: + - "python" + message: | + The application is generating an RSA key that is less than the recommended 2048 bits. + The National Institute of Standards and Technology (NIST) deprecated signing Digital + Certificates that contained RSA Public Keys of 1024 bits in December 2010. While + 1024-bit RSA keys have not been factored yet, advances in compute may make it possible + in the near future. + + Consider upgrading to the newer asymmetric algorithm such as `X25519` which handles + the complexities of generating key pairs and choosing correct key sizes for you: + ``` + from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey + + # Generate a private key for use in the exchange. + private_key = X25519PrivateKey.generate() + # Work with private key/exchange with a peer's + # public key to created a shared and derived key + # ... + ``` + + Otherwise use a key size greater than 2048 when generating RSA keys: + ``` + from cryptography.hazmat.primitives.asymmetric import rsa + # Generate a private key of 4096 bits + private_key = rsa.generate_private_key( + # do not change the exponent value from 65537 + public_exponent=65537, + key_size=4096, + ) + # Work with the private key to sign/encrypt data + # ... + ``` + + For more information on using the cryptography module see: + - https://cryptography.io/en/latest + metadata: + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Inadequate encryption strength" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: | + cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(...,key_size=$SIZE,...) + - pattern: | + cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key($EXP, $SIZE,...) + - pattern: | + cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key($SIZE, ...) + - pattern: | + cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(...,key_size=$SIZE,...) + - pattern: | + cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key($EXP, $SIZE, ...) + - pattern: "cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key($SIZE,...)" + - pattern: "Crypto.PublicKey.RSA.generate($SIZE, ...)" + - pattern: "Crypto.PublicKey.DSA.generate($SIZE, ...)" + - pattern: "Cryptodome.PublicKey.DSA.generate($SIZE, ...)" + - pattern: "Cryptodome.PublicKey.RSA.generate($SIZE, ...)" + - pattern: "Crypto.PublicKey.DSA.generate(bits=$SIZE, ...)" + - pattern: "Cryptodome.PublicKey.DSA.generate(bits=$SIZE, ...)" + - pattern: "pycrypto_rsa.generate(bits=$SIZE, ...)" + - pattern: "pycrypto_dsa.generate(bits=$SIZE, ...)" + - pattern: "pycryptodomex_rsa.generate(bits=$SIZE, ...)" + - pattern: "pycryptodomex_rsa.generate($SIZE, ...)" + - pattern: "pycryptodomex_dsa.generate(bits=$SIZE, ...)" + - pattern: "pycryptodomex_dsa.generate($SIZE, ...)" + - metavariable-comparison: + comparison: "$SIZE < 2048" + metavariable: "$SIZE" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-encrypt-ec.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-encrypt-ec.yml new file mode 100644 index 0000000..28f3de6 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-encrypt-ec.yml @@ -0,0 +1,49 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-encrypt-ec" + languages: + - "python" + message: | + The application was found using an insufficient curve size for the Elliptical + Cryptography (EC) asymmetric algorithm. NIST recommends using a key size of + 224 or greater. + + To remediate this issue, replace the current key size with `ec.SECP384R1`, + + Example using `ec.SECP384R1`: + ``` + from cryptography.hazmat.primitives.asymmetric import ec + # Generate an EC private key using SECP384R1 + private_key = ec.generate_private_key( + ec.SECP384R1() + ) + # Work with/sign data using the key + # ... + ``` + + For more information on the cryptography module's EC section see: + - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/ + metadata: + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Inadequate encryption strength" + security-severity: "Medium" + patterns: + - pattern-inside: |- + cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(...) + - pattern: "cryptography.hazmat.primitives.asymmetric.ec.$SIZE" + - metavariable-pattern: + metavariable: "$SIZE" + pattern-either: + - pattern: "SECP192R1" + - pattern: "SECT163K1" + - pattern: "SECT163R2" + - focus-metavariable: "$SIZE" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hash-md5.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hash-md5.yml new file mode 100644 index 0000000..c218737 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hash-md5.yml @@ -0,0 +1,56 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-hash-md5" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD5 + and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + It is strongly recommended that a standard digest algorithm be chosen instead as implementing + a custom algorithm is prone to errors. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example of creating a SHA-384 hash using the `cryptography` package: + ``` + from cryptography.hazmat.primitives import hashes + # Create a SHA384 digest + digest = hashes.Hash(hashes.SHA384()) + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + result = digest.finalize() + ``` + + For more information on secure password storage see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Crypto.Hash.MD5.new(...)" + - pattern: "Cryptodome.Hash.MD5.new (...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hash-sha1.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hash-sha1.yml new file mode 100644 index 0000000..2270e61 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hash-sha1.yml @@ -0,0 +1,56 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-hash-sha1" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD5 + and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + It is strongly recommended that a standard digest algorithm be chosen instead as implementing + a custom algorithm is prone to errors. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example of creating a SHA-384 hash using the `cryptography` package: + ``` + from cryptography.hazmat.primitives import hashes + # Create a SHA384 digest + digest = hashes.Hash(hashes.SHA384()) + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + result = digest.finalize() + ``` + + For more information on secure password storage see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Crypto.Hash.SHA.new(...)" + - pattern: "Cryptodome.Hash.SHA.new (...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-arc4.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-arc4.yml new file mode 100644 index 0000000..e93a94c --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-arc4.yml @@ -0,0 +1,74 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-hazmat-cipher-arc4" + languages: + - "python" + message: | + DES, TripleDES, RC2 and RC4 are all considered broken or insecure cryptographic algorithms. + Newer algorithms apply message integrity to validate ciphertext has not been tampered + with. Consider using `ChaCha20Poly1305` instead as it is easier and faster than the + alternatives such as `AES-256-GCM`. + + For older applications that don't have support for `ChaCha20Poly1305`, + `AES-256-GCM` is recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + pattern: "cryptography.hazmat.primitives.ciphers.algorithms.ARC4(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-blowfish.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-blowfish.yml new file mode 100644 index 0000000..e852050 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-blowfish.yml @@ -0,0 +1,76 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-hazmat-cipher-blowfish" + languages: + - "python" + message: | + The Blowfish encryption algorithm was meant as a drop-in replacement for DES and was created in + 1993. Smaller key sizes may make the ciphertext vulnerable to [birthday + attacks](https://en.wikipedia.org/wiki/Birthday_attack). While no known attacks against + Blowfish + exist, it should never be used to encrypt files over 4GB in size. If possible consider + using ChaCha20Poly1305 or AES-GCM instead of Blowfish. + + For older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is + recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + pattern: "cryptography.hazmat.primitives.ciphers.algorithms.Blowfish(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-idea.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-idea.yml new file mode 100644 index 0000000..dc07e18 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-cipher-idea.yml @@ -0,0 +1,76 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto-hazmat-cipher-idea" + languages: + - "python" + message: | + The IDEA encryption algorithm was meant as a drop-in replacement for DES and was created in + 1991. A number of [vulnerabilities and + exploits](https://en.wikipedia.org/wiki/International_Data_Encryption_Algorithm#Security) have + been identified to work against IDEA and + it is no longer recommended. If possible consider + using ChaCha20Poly1305 or AES-GCM instead of Blowfish. + + For older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is + recommended, however it has many drawbacks: + - Slower than `ChaCha20Poly1305`. + - Catastrophic failure if nonce values are reused. + + Example using `ChaCha20Poly1305`: + ``` + import os + # Import ChaCha20Poly1305 from cryptography + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = ChaCha20Poly1305.generate_key() + # Create a new ChaCha20Poly1305 instance with our secure key + chacha = ChaCha20Poly1305(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = chacha.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + chacha.decrypt(nonce, cipher_text, aad) + ``` + + Example using `AESGCM`: + ``` + import os + # Import AESGCM from cryptography + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # Our plaintext to encrypt + plain_text = b"Secret text to encrypt" + # We do not require authenticated but unencrypted data, so set to None + aad = None + # Generate a secure key + key = AESGCM.generate_key(bit_length=128) + # Create a new AESGCM instance with our secure key + aesgcm = AESGCM(key) + # Note: nonce values _must_ be regenerated every time they are used. + nonce = os.urandom(12) + # Encrypt our plaintext + cipher_text = aesgcm.encrypt(nonce, plain_text, aad) + # Decrypt the plain text using the nonce and cipher_text + aesgcm.decrypt(nonce, cipher_text, aad) + ``` + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Use of a Broken or Risky Cryptographic Algorithm" + security-severity: "Medium" + category: "security" + pattern: "cryptography.hazmat.primitives.ciphers.algorithms.IDEA(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-hash-md5.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-hash-md5.yml new file mode 100644 index 0000000..8427a10 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-hash-md5.yml @@ -0,0 +1,50 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto.hazmat-hash-md5" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD2, MD5 + and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + It is strongly recommended that a standard digest algorithm be chosen instead as implementing + a custom algorithm is prone to errors. + + Example of creating a SHA-384 hash using the `cryptography` package: + ``` + from cryptography.hazmat.primitives import hashes + # Create a SHA384 digest + digest = hashes.Hash(hashes.SHA384()) + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + result = digest.finalize() + ``` + + For more information on secure password storage see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + category: "security" + pattern: "cryptography.hazmat.primitives.hashes.MD5(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-hash-sha1.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-hash-sha1.yml new file mode 100644 index 0000000..7a5a83c --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-crypto-hazmat-hash-sha1.yml @@ -0,0 +1,50 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-crypto.hazmat-hash-sha1" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD2, MD5 + and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + It is strongly recommended that a standard digest algorithm be chosen instead as implementing + a custom algorithm is prone to error. + + Example of creating a SHA-384 hash using the `cryptography` package: + ``` + from cryptography.hazmat.primitives import hashes + # Create a SHA384 digest + digest = hashes.Hash(hashes.SHA384()) + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + result = digest.finalize() + ``` + + For more information on secure password storage see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + pattern: "cryptography.hazmat.primitives.hashes.SHA1(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md2.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md2.yml new file mode 100644 index 0000000..2f969f1 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md2.yml @@ -0,0 +1,54 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-hash-md2" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD2, MD5 + and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example of creating a SHA-384 hash using the `cryptography` package: + ``` + from cryptography.hazmat.primitives import hashes + # Create a SHA384 digest + digest = hashes.Hash(hashes.SHA384()) + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + result = digest.finalize() + ``` + + For more information on secure password storage see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Crypto.Hash.MD2.new(...)" + - pattern: "Cryptodome.Hash.MD2.new (...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md4.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md4.yml new file mode 100644 index 0000000..fc48267 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md4.yml @@ -0,0 +1,54 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-hash-md4" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD2, MD4, + MD5 and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example of creating a SHA-384 hash using the `cryptography` package: + ``` + from cryptography.hazmat.primitives import hashes + # Create a SHA384 digest + digest = hashes.Hash(hashes.SHA384()) + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + result = digest.finalize() + ``` + + For more information on secure password storage see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "Crypto.Hash.MD4.new(...)" + - pattern: "Cryptodome.Hash.MD4.new (...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md5.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md5.yml new file mode 100644 index 0000000..9374e34 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-hash-md5.yml @@ -0,0 +1,51 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-hash-md5" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD2, MD4, + MD5 and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example of creating a SHA-384 hash using the `cryptography` package: + ``` + from cryptography.hazmat.primitives import hashes + # Create a SHA384 digest + digest = hashes.Hash(hashes.SHA384()) + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + result = digest.finalize() + ``` + + For more information on secure password storage see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + pattern: "hashlib.md5(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-hash-sha1.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-hash-sha1.yml new file mode 100644 index 0000000..e078474 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-hash-sha1.yml @@ -0,0 +1,51 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-hash-sha1" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD2, MD4, + MD5 and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + + Note that the `Crypto` and `Cryptodome` Python packages are no longer recommended for + new applications, instead consider using the [cryptography](https://cryptography.io/) package. + + Example of creating a SHA-384 hash using the `cryptography` package: + ``` + from cryptography.hazmat.primitives import hashes + # Create a SHA384 digest + digest = hashes.Hash(hashes.SHA384()) + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + result = digest.finalize() + ``` + + For more information on secure password storage see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + + For more information on the cryptography module see: + - https://cryptography.io/en/latest/ + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + pattern: "hashlib.sha1(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-hashlib-new-insecure-functions.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-hashlib-new-insecure-functions.yml new file mode 100644 index 0000000..2c772d2 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-hashlib-new-insecure-functions.yml @@ -0,0 +1,51 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-hashlib-new-insecure-functions" + languages: + - "python" + message: | + The application was found using an insecure or risky digest or signature algorithm. MD2, MD4, + MD5 and SHA1 hash algorithms have been found to be vulnerable to producing collisions. + + This means + that two different values, when hashed, can lead to the same hash value. If the application is + trying + to use these hash methods for storing passwords, then it is recommended to switch to a + password hashing + algorithm such as Argon2id or PBKDF2. + It is strongly recommended that a standard digest algorithm be chosen instead as implementing + a custom algorithm is prone to errors. + + Example using `hashlib.sha384()` to create a SHA384 hash: + ``` + import hashlib + # Create a SHA384 digest + digest = hashlib.sha384() + # Update the digest with some initial data + digest.update(b"some data to hash") + # Add more data to the digest + digest.update(b"some more data") + # Finalize the digest as bytes + digest.digest() + ``` + metadata: + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of a broken or risky cryptographic algorithm" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "hashlib.new(\"=~/[M|m][D|d][4|5]/\", ...)" + - pattern: "hashlib.new(..., name=\"=~/[M|m][D|d][4|5]/\", ...)" + - pattern: "hashlib.new('sha1')" + - pattern: "hashlib.new(..., name='SHA1')" + - pattern: "hashlib.new('sha', string='test')" + - pattern: "hashlib.new(name='SHA', string='test')" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/crypto/rule-import-pycrypto.yml b/data/rules/gitlab-sast-rules/python/crypto/rule-import-pycrypto.yml new file mode 100644 index 0000000..2d47335 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/crypto/rule-import-pycrypto.yml @@ -0,0 +1,35 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_crypto_rule-import-pycrypto" + languages: + - "python" + message: | + The application was detected importing `pycrypto`. This package has been deprecated as it + contains + security vulnerabilities. + + To remediate this issue, consider using the [cryptography](https://cryptography.io/) + package instead. + metadata: + cwe: "CWE-1104" + owasp: + - "A9:2017-Using Components with Known Vulnerabilities" + - "A06:2021-Vulnerable and Outdated Components" + - "A03:2025-Software Supply Chain Failures" + category: "security" + shortDescription: "Use of unmaintained third party components" + security-severity: "Medium" + pattern-either: + - pattern: "import pycryto" + - pattern: "import Crypto.Cipher" + - pattern: "import Crypto.Hash" + - pattern: "import Crypto.IO" + - pattern: "import Crypto.Protocol" + - pattern: "import Crypto.PublicKey" + - pattern: "import Crypto.Random" + - pattern: "import Crypto.Signature" + - pattern: "import Crypto.Util" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/deserialization/rule-cpickle.yml b/data/rules/gitlab-sast-rules/python/deserialization/rule-cpickle.yml new file mode 100644 index 0000000..1d93799 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/deserialization/rule-cpickle.yml @@ -0,0 +1,77 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_deserialization_rule-cpickle" + languages: + - "python" + message: | + The application was found using `cPickle` which is vulnerable to deserialization attacks. + Deserialization attacks exploit the process of reading serialized data and turning it back + into an object. By constructing malicious objects and serializing them, an adversary may + attempt to: + + - Inject code that is executed upon object construction, which occurs during the + deserialization process. + - Exploit mass assignment by including fields that are not normally a part of the serialized + data but are read in during deserialization. + + Consider safer alternatives such as serializing data in the JSON format. Ensure any format + chosen allows + the application to specify exactly which object types are allowed to be deserialized. + + To protect against mass assignment, only allow deserialization of the specific fields that are + required. If this is not easily done, consider creating an intermediary type that + can be serialized with only the necessary fields exposed. + + Example JSON deserializer using an intermediary type that is validated against a schema to + ensure + it is safe from mass assignment: + ``` + import jsonschema + + # Create a schema to validate our user-supplied input against + # an intermediary object + intermediary_schema = { + "type" : "object", + "properties" : { + "name": {"type" : "string"} + }, + "required": ["name"], + # Protect against random properties being added to the object + "additionalProperties": False, + } + # If a user attempted to add "'is_admin': True" it would cause a validation error + intermediary_object = {'name': 'test user'} + + try: + # Validate the user supplied intermediary object against our schema + jsonschema.validate(instance=intermediary_object, schema=intermediary_schema) + user_object = {'user': + { + # Assign the deserialized data from intermediary object + 'name': intermediary_object['name'], + # Add in protected data in object definition (or set it from a class constructor) + 'is_admin': False, + } + } + # Work with the user_object + except jsonschema.exceptions.ValidationError as ex: + # Gracefully handle validation errors + # ... + ``` + + For more details on deserialization attacks in general, see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html + metadata: + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + category: "security" + shortDescription: "Deserialization of untrusted data" + security-severity: "High" + pattern: "cPickle.$FUNC(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/deserialization/rule-dill.yml b/data/rules/gitlab-sast-rules/python/deserialization/rule-dill.yml new file mode 100644 index 0000000..374b480 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/deserialization/rule-dill.yml @@ -0,0 +1,79 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_deserialization_rule-dill" + languages: + - "python" + message: | + The application was found using `dill` which is vulnerable to deserialization attacks. + Deserialization attacks exploit the process of reading serialized data and turning it back + into an object. By constructing malicious objects and serializing them, an adversary may + attempt to: + + - Inject code that is executed upon object construction, which occurs during the + deserialization process. + - Exploit mass assignment by including fields that are not normally a part of the serialized + data but are read in during deserialization. + + Consider safer alternatives such as serializing data in the JSON format. Ensure any format + chosen allows + the application to specify exactly which object types are allowed to be deserialized. + + To protect against mass assignment, only allow deserialization of the specific fields that are + required. If this is not easily done, consider creating an intermediary type that + can be serialized with only the necessary fields exposed. + + Example JSON deserializer using an intermediary type that is validated against a schema to + ensure + it is safe from mass assignment: + ``` + import jsonschema + + # Create a schema to validate our user-supplied input against + # an intermediary object + intermediary_schema = { + "type" : "object", + "properties" : { + "name": {"type" : "string"} + }, + "required": ["name"], + # Protect against random properties being added to the object + "additionalProperties": False, + } + # If a user attempted to add "'is_admin': True" it would cause a validation error + intermediary_object = {'name': 'test user'} + + try: + # Validate the user supplied intermediary object against our schema + jsonschema.validate(instance=intermediary_object, schema=intermediary_schema) + user_object = {'user': + { + # Assign the deserialized data from intermediary object + 'name': intermediary_object['name'], + # Add in protected data in object definition (or set it from a class constructor) + 'is_admin': False, + } + } + # Work with the user_object + except jsonschema.exceptions.ValidationError as ex: + # Gracefully handle validation errors + # ... + ``` + + For more details on deserialization attacks in general, see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html + metadata: + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + category: "security" + shortDescription: "Deserialization of untrusted data" + security-severity: "High" + patterns: + - pattern: dill.$FUNC(...) + - pattern-not: dill.$FUNC("...") + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/deserialization/rule-marshal.yml b/data/rules/gitlab-sast-rules/python/deserialization/rule-marshal.yml new file mode 100644 index 0000000..f64fba6 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/deserialization/rule-marshal.yml @@ -0,0 +1,81 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_deserialization_rule-marshal" + languages: + - "python" + message: | + The application was found using `dill` which is vulnerable to deserialization attacks. + Deserialization attacks exploit the process of reading serialized data and turning it back + into an object. By constructing malicious objects and serializing them, an adversary may + attempt to: + + - Inject code that is executed upon object construction, which occurs during the + deserialization process. + - Exploit mass assignment by including fields that are not normally a part of the serialized + data but are read in during deserialization. + + Consider safer alternatives such as serializing data in the JSON format. Ensure any format + chosen allows + the application to specify exactly which object types are allowed to be deserialized. + + To protect against mass assignment, only allow deserialization of the specific fields that are + required. If this is not easily done, consider creating an intermediary type that + can be serialized with only the necessary fields exposed. + + Example JSON deserializer using an intermediary type that is validated against a schema to + ensure + it is safe from mass assignment: + ``` + import jsonschema + + # Create a schema to validate our user-supplied input against + # an intermediary object + intermediary_schema = { + "type" : "object", + "properties" : { + "name": {"type" : "string"} + }, + "required": ["name"], + # Protect against random properties being added to the object + "additionalProperties": False, + } + # If a user attempted to add "'is_admin': True" it would cause a validation error + intermediary_object = {'name': 'test user'} + + try: + # Validate the user supplied intermediary object against our schema + jsonschema.validate(instance=intermediary_object, schema=intermediary_schema) + user_object = {'user': + { + # Assign the deserialized data from intermediary object + 'name': intermediary_object['name'], + # Add in protected data in object definition (or set it from a class constructor) + 'is_admin': False, + } + } + # Work with the user_object + except jsonschema.exceptions.ValidationError as ex: + # Gracefully handle validation errors + # ... + ``` + + For more details on deserialization attacks in general, see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html + metadata: + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + category: "security" + shortDescription: "Deserialization of untrusted data" + security-severity: "High" + pattern-either: + - pattern: "marshal.dump(...)" + - pattern: "marshal.dumps(...)" + - pattern: "marshal.load(...)" + - pattern: "marshal.loads(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/deserialization/rule-pickle.yml b/data/rules/gitlab-sast-rules/python/deserialization/rule-pickle.yml new file mode 100644 index 0000000..11eddb3 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/deserialization/rule-pickle.yml @@ -0,0 +1,85 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_deserialization_rule-pickle" + languages: + - "python" + message: | + The application was found using `pickle` which is vulnerable to deserialization attacks. + Deserialization attacks exploit the process of reading serialized data and turning it back + into an object. By constructing malicious objects and serializing them, an adversary may + attempt to: + + - Inject code that is executed upon object construction, which occurs during the + deserialization process. + - Exploit mass assignment by including fields that are not normally a part of the serialized + data but are read in during deserialization. + + Consider safer alternatives such as serializing data in the JSON format. Ensure any format + chosen allows the application to specify exactly which object types are allowed to be deserialized. + + To protect against mass assignment, only allow deserialization of the specific fields that are + required. If this is not easily done, consider creating an intermediary type that + can be serialized with only the necessary fields exposed. + + Example JSON deserializer using an intermediary type that is validated against a schema to ensure + it is safe from mass assignment: + ``` + import jsonschema + + # Create a schema to validate our user-supplied input against + # an intermediary object + intermediary_schema = { + "type" : "object", + "properties" : { + "name": {"type" : "string"} + }, + "required": ["name"], + # Protect against random properties being added to the object + "additionalProperties": False, + } + # If a user attempted to add "'is_admin': True" it would cause a validation error + intermediary_object = {'name': 'test user'} + + try: + # Validate the user supplied intermediary object against our schema + jsonschema.validate(instance=intermediary_object, schema=intermediary_schema) + user_object = {'user': + { + # Assign the deserialized data from intermediary object + 'name': intermediary_object['name'], + # Add in protected data in object definition (or set it from a class constructor) + 'is_admin': False, + } + } + # Work with the user_object + except jsonschema.exceptions.ValidationError as ex: + # Gracefully handle validation errors + # ... + ``` + + For more details on deserialization attacks in general, see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html + metadata: + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + category: "security" + shortDescription: "Deserialization of untrusted data" + security-severity: "High" + patterns: + - pattern-either: + - patterns: + - pattern: "pickle.$METHOD(...)" + - pattern-not: "pickle.$METHOD(\"...\")" + - patterns: + - pattern: "_pickle.$METHOD(...)" + - pattern-not: "_pickle.$METHOD(\"...\")" + - metavariable-regex: + metavariable: "$METHOD" + regex: "(load|loads|Unpickler)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/deserialization/rule-shelve.yml b/data/rules/gitlab-sast-rules/python/deserialization/rule-shelve.yml new file mode 100644 index 0000000..a6f39ea --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/deserialization/rule-shelve.yml @@ -0,0 +1,79 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_deserialization_rule-shelve" + languages: + - "python" + message: | + The application was found using `shelve` which is vulnerable to deserialization attacks as + it calls `pickle` internally. + Deserialization attacks exploit the process of reading serialized data and turning it back + into an object. By constructing malicious objects and serializing them, an adversary may + attempt to: + + - Inject code that is executed upon object construction, which occurs during the + deserialization process. + - Exploit mass assignment by including fields that are not normally a part of the serialized + data but are read in during deserialization. + + Consider safer alternatives such as serializing data in the JSON format. Ensure any format + chosen allows + the application to specify exactly which object types are allowed to be deserialized. + + To protect against mass assignment, only allow deserialization of the specific fields that are + required. If this is not easily done, consider creating an intermediary type that + can be serialized with only the necessary fields exposed. + + Example JSON deserializer using an intermediary type that is validated against a schema to + ensure + it is safe from mass assignment: + ``` + import jsonschema + + # Create a schema to validate our user-supplied input against + # an intermediary object + intermediary_schema = { + "type" : "object", + "properties" : { + "name": {"type" : "string"} + }, + "required": ["name"], + # Protect against random properties being added to the object + "additionalProperties": False, + } + # If a user attempted to add "'is_admin': True" it would cause a validation error + intermediary_object = {'name': 'test user'} + + try: + # Validate the user supplied intermediary object against our schema + jsonschema.validate(instance=intermediary_object, schema=intermediary_schema) + user_object = {'user': + { + # Assign the deserialized data from intermediary object + 'name': intermediary_object['name'], + # Add in protected data in object definition (or set it from a class constructor) + 'is_admin': False, + } + } + # Work with the user_object + except jsonschema.exceptions.ValidationError as ex: + # Gracefully handle validation errors + # ... + ``` + + For more details on deserialization attacks in general, see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html + metadata: + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + category: "security" + shortDescription: "Deserialization of untrusted data" + security-severity: "High" + pattern-either: + - pattern: "shelve.$FUNC(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/deserialization/rule-yaml-load.yml b/data/rules/gitlab-sast-rules/python/deserialization/rule-yaml-load.yml new file mode 100644 index 0000000..1733c11 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/deserialization/rule-yaml-load.yml @@ -0,0 +1,73 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_deserialization_rule-yaml-load" + languages: + - "python" + message: | + The application was found using an unsafe version of `yaml` load which is vulnerable to + deserialization attacks. Deserialization attacks exploit the process of reading serialized + data and turning it back + into an object. By constructing malicious objects and serializing them, an adversary may + attempt to: + + - Inject code that is executed upon object construction, which occurs during the + deserialization process. + - Exploit mass assignment by including fields that are not normally a part of the serialized + data but are read in during deserialization. + + To remediate this issue, use `safe_load()` or call `yaml.load()` with the `Loader` argument + set to + `yaml.SafeLoader`. + + Example loading YAML using `safe_load`: + ``` + import yaml + + # Use safe_load to load data into an intermediary object + intermediary_object = yaml.safe_load("""user: + name: 'test user'""" + ) + # Create our real object, copying over only the necessary fields + user_object = {'user': { + # Assign the deserialized data from intermediary object + 'name': intermediary_object['user']['name'], + # Add in protected data in object definition (or set it from a class constructor) + 'is_admin': False, + } + } + # Work with user_object + # ... + ``` + + For more details on deserialization attacks in general, see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html + patterns: + - pattern-inside: | + import yaml + ... + - pattern-not-inside: | + from ruamel.yaml import YAML + ... + - pattern-either: + - pattern: "yaml.unsafe_load(...)" + - pattern: "yaml.$LD(..., Loader=yaml.$LOADER, ...)" + - pattern: "yaml.$LD($DATA)" + - metavariable-regex: + metavariable: "$LOADER" + regex: "(Loader|UnsafeLoader|CLoader|FullLoader)" + - metavariable-regex: + metavariable: "$LD" + regex: "(load|load_all)" + metadata: + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + category: "security" + shortDescription: "Deserialization of untrusted data" + security-severity: "High" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/django/rule-django-extra-used.yml b/data/rules/gitlab-sast-rules/python/django/rule-django-extra-used.yml new file mode 100644 index 0000000..4adbb2c --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/django/rule-django-extra-used.yml @@ -0,0 +1,91 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_django_rule-django-extra-used" + languages: + - "python" + message: | + SQL Injection is a critical vulnerability that can lead to data or system compromise. By + dynamically generating SQL query strings, user input may be able to influence the logic of + the SQL statement. This could lead to an adversary accessing information they should + not have access to, or in some circumstances, being able to execute OS functionality or code. + + Replace all dynamically generated SQL queries with parameterized queries. In situations where + dynamic queries must be created, never use direct user input, but instead use a map or + dictionary of valid values and resolve them using a user supplied key. + + For example, some database drivers do not allow parameterized queries for `>` or `<` comparison + operators. In these cases, do not use a user supplied `>` or `<` value, but rather have the + user + supply a `gt` or `lt` value. The alphabetical values are then used to look up the `>` and `<` + values to be used in the construction of the dynamic query. The same goes for other queries + where + column or table names are required but cannot be parameterized. + + The `QuerySet.extra` API method will be deprecated as it a source of SQL Injection + vulnerabilities and other problems. This method is especially risky as callers + will need to do their own escaping of any parameters that come from user-supplied + information. + + To remediate this issue, do not use `extra` but use other `QuerySet` methods to achieve + the same goals. If for some reason this is not feasible, consider using the `RawSQL` method + and making sure that all arguments, including user-supplied ones, are only used in + `params` + + + While not recommended due to [potential SQL + Injection](https://docs.djangoproject.com/en/4.2/ref/models/expressions/#raw-sql-expressions), + below is an example using `RawSQL`, + passing in user-supplied data as a `param` which will escape the input: + ``` + # If dealing with integer based user input, restrict the values to integers only using the + # path configuration: path('/someview/', views.some_view, + name='someview'), + + # views.py + def some_view(request, user_supplied_id): + # Never use string interpolation in the `sql` parameter. + # Never quote the `%s` string format such as `... where id='%s'` as this could lead to SQL + Injection. + # Pass the user supplied data only in the `params` parameter. + for obj in DBObject.objects.all().annotate( + val=RawSQL(sql="select id from some_secondary_table where id=%s", + params=[user_supplied_id])): + # Work with the results from the query + # ... + ``` + + For more information on QuerySet see: + - https://docs.djangoproject.com/en/4.2/ref/models/querysets/#queryset-api + + For more information on SQL Injection see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html + patterns: + - pattern: "$X.objects. ... .extra(..., $K = $V, ...)" + - pattern-not-inside: | + $V = ['...'] + ... + - metavariable-pattern: + metavariable: "$V" + patterns: + - pattern: "$V" + - pattern-not: "[..., '...', ...]" + - pattern-not: "{..., '...': '...', ...}" + - pattern-not: "\"...\"" + - pattern-not: "[..., \"...\" % \"...\", ...]" + - pattern-not: "{..., $L: \"...\" % \"...\", ...}" + - pattern-not: "{..., $L: \"...\".format(\"...\"), ...}" + - pattern-not: "[..., \"...\".format(\"...\"), ...]" + metadata: + cwe: "CWE-89" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + category: "security" + shortDescription: "Improper neutralization of special elements used in an SQL + Command ('SQL Injection')" + security-severity: "High" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/escaping/rule-jinja2-autoescape-false.yml b/data/rules/gitlab-sast-rules/python/escaping/rule-jinja2-autoescape-false.yml new file mode 100644 index 0000000..a0a7c6a --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/escaping/rule-jinja2-autoescape-false.yml @@ -0,0 +1,168 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_escaping_rule-jinja2-autoescape-false" + languages: + - "python" + message: | + The application was found using Jinja2 `Environment` without autoescaping enabled. If using in + the context of HTML this could lead to Cross-Site Scripting (XSS) attacks when rendering with + user-supplied input. + + Unfortunately, Jinja2 does not support context-aware escaping, meaning it is insufficient to + protect against + XSS for the various web contexts. It is important to encode the data depending on the specific + context + it + is used in. There are at least six context types: + + - Inside HTML tags `
context 1
` + - Inside attributes: `
` + - Inside event attributes `` + - Inside script blocks: `` + - Unsafe element HTML assignment: `element.innerHTML = "context 5"` + - Inside URLs: `link` + + Script blocks alone have multiple ways they need to be encoded. Extra care must be taken if + user input + is ever output inside of script tags. + + User input that is displayed within the application must be encoded, sanitized or validated + to ensure it cannot be treated as HTML or executed as Javascript code. Care must also be + taken + to not mix server-side templating with client-side templating, as the server-side templating + will + not encode things like {{ 7*7 }} which may execute client-side templating features. + + It is _NOT_ advised to encode user input prior to inserting into a data store. The data will + need to be + encoded depending on context of where it is output. It is much safer to force the displaying + system to + handle the encoding and not attempt to guess how it should be encoded. + + To handle different contexts, one approach would be to write custom Jinja2 filters. Below is + an example + that escapes or encodes links and potentially malicious script, note this does not include + other contexts + such as CSS or attributes: + ``` + from jinja2 import Environment, select_autoescape, FileSystemLoader + from jinja2 import pass_eval_context + from markupsafe import Markup, escape + + @pass_eval_context + def escape_link(eval_ctx, value): + bad_link = "#JinjatmplZ" + # Block any values that start with // as that could be used to inject + # links to third party pages see: + https://en.wikipedia.org/wiki/Wikipedia:Protocol-relative_URL + if value.startswith('//'): + return bad_link + + # Only allow relative links + # if you want to allow links that start with http or ws replace with below: + # if not value.startswith('/'): and not value.startswith('http') and not + value.startswith('ws') + if not value.startswith('/'): + return bad_link + + # Alternatively, you could only call escape if autoescape is true + # if eval_ctx.autoescape: + # return escape(value) + # else + # return value + + return escape(value) + + # Create a replacement table + js_replacement = str.maketrans({ + '"': "\\u0022", + '`': "\\u0060", + '&': "\\u0026", + '\'': "\\u0027", + '+': "\\u002b", + '/': "\\/", + '<': "\\u003c", + '>': "\\u003e", + '\\': "\\\\", + '(': "\\u0028", + ')': "\\u0029", + }) + + @pass_eval_context + def escape_js(eval_ctx, value): + """ + Escape the input for use in ", + script_context="alert(1);alert`1`",) + ) + + # Sample template: + """ + + + + My Webpage + + +

My Webpage

+ {{ html_context }} + link + + + + """ + ``` + + For more information on autoescape see: + - https://jinja.palletsprojects.com/en/3.1.x/api/#autoescaping + + For more information on XSS see OWASP: + - https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html + metadata: + cwe: "CWE-116" + owasp: + - "A7:2017-Cross-Site Scripting (XSS)" + - "A03:2021-Injection" + - "A05:2025-Injection" + category: "security" + shortDescription: "Improper encoding or escaping of output" + security-severity: "Medium" + patterns: + - pattern-inside: | + import jinja2 + ... + - pattern-not: "jinja2.Environment(..., autoescape=True, ...)" + - pattern-not: "jinja2.Environment(..., autoescape=jinja2.select_autoescape(...),...)" + - pattern-not: "jinja2.Environment(..., autoescape=select_autoescape(...), ...)" + - pattern: "jinja2.Environment(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/escaping/rule-use-of-mako-templates.yml b/data/rules/gitlab-sast-rules/python/escaping/rule-use-of-mako-templates.yml new file mode 100644 index 0000000..826a1f6 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/escaping/rule-use-of-mako-templates.yml @@ -0,0 +1,147 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_escaping_rule-use-of-mako-templates" + languages: + - "python" + message: | + The application was found using mako templates without `default_filters` + being passed to the `Template` or `TemplateLookup` constructors. If using + in the context of HTML, this could lead to Cross-Site Scripting (XSS) attacks + when rendering with user-supplied input. + + Unfortunately, Jinja2 does not support context-aware escaping, meaning it + is insufficient to protect against XSS for the various web contexts. It is + important to encode the data depending on the specific context it is used in. + There are at least six context types: + + - Inside HTML tags `
context 1
` + - Inside attributes: `
` + - Inside event attributes `` + - Inside script blocks: `` + - Unsafe element HTML assignment: `element.innerHTML = "context 5"` + - Inside URLs: + `link` + + Script blocks alone have multiple ways they need to be encoded. Extra care + must be taken if user input is ever output inside of script tags. + + User input that is displayed within the application must be encoded, + sanitized or validated to ensure it cannot be treated as HTML or executed + as Javascript code. Care must also be taken to not mix server-side templating + with client-side templating, as the server-side templating will not encode things + like {{ 7*7 }} which may execute client-side templating features. + + It is _NOT_ advised to encode user input prior to inserting into a data + store. The data will need to be encoded depending on context of where it is output. + It is much safer to force the displaying system to handle the encoding and + not attempt to guess how it should be encoded. + + To handle different contexts, one approach would be to write custom mako + filters. Below is an example that escapes or encodes links and + potentially malicious script, note this does not include other contexts + such as CSS or attributes: + ``` + # filters.py module: + + def escape_link(value): + bad_link = "#JinjatmplZ" + # Block any values that start with // as that could be used to inject + # links to third party pages see: + https://en.wikipedia.org/wiki/Wikipedia:Protocol-relative_URL + if value.startswith('//'): + return bad_link + + # Only allow relative links + # if you want to allow links that start with http or ws replace with below: + # if not value.startswith('/'): and not value.startswith('http') and not + value.startswith('ws') + if not value.startswith('/'): + return bad_link + + return value + + # Create a replacement table + js_replacement = str.maketrans({ + '\0': "\\u0000", + '\t': "\\t", + '\n': "\\n", + '\v': "\\u000b", + '\f': "\\f`", + '\r': "\\r", + '"': "\\u0022", + '`': "\\u0060", + '&': "\\u0026", + '\'': "\\u0027", + '+': "\\u002b", + '/': "\\/", + '<': "\\u003c", + '>': "\\u003e", + '\\': "\\\\", + '(': "\\u0028", + ')': "\\u0029", + }) + + def escape_js(value): + # Escape the input for use in + + + """ + + # Load our template with default filters and our imported filters for + # usage in template files + t = Template(template_text, + # By default enable the html filter with 'h' + default_filters=['h'], + # Import our custom filters + imports=["from filters import escape_link, escape_js"]) + + # Render our template + print(t.render(html_context="", + link_context="/# onclick=alert(1)", + script_context="alert(1)",) + ) + ``` + metadata: + cwe: "CWE-79" + category: "security" + owasp: + - "A7:2017-Cross-Site Scripting (XSS)" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of input during web page generation ('Cross-site Scripting')" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "mako.template.Template(...)" + - pattern: "mako.lookup.TemplateLookup(...)" + - pattern-not: "mako.lookup.TemplateLookup(..., default_filters=[\"...\"])" + - pattern-not: "mako.template.Template(..., default_filters=[\"...\"])" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/eval/rule-eval.yml b/data/rules/gitlab-sast-rules/python/eval/rule-eval.yml new file mode 100644 index 0000000..359cce5 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/eval/rule-eval.yml @@ -0,0 +1,53 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_eval_rule-eval" + languages: + - "python" + message: | + The application was found calling the `eval` function with non-literal data. If the variable contains + user-controlled data, either partially or fully, an adversary could compromise the entire system by + executing arbitrary Python code. + + To remediate this issue, remove all calls to `eval` and consider alternative methods for executing + the necessary business logic. There is almost no safe method of calling `eval` with user-supplied input. + + If the application only needs to convert strings into objects, consider using `json.loads`. In + some cases `ast.literal_eval` is recommended, but this should be avoided as it can still suffer + from other issues such as the ability for malicious code to crash the python interpreter or application. + + Example using `json.loads`` to load in arbitrary data to create data structures: + ``` + # User supplied data as a blob of JSON + user_supplied_data = """{"user": "test", "metadata": [1,2,3]}""" + # Load the JSON + user_object = json.loads(user_supplied_data) + # Manually add protected properties _after_ loading, never before + user_object["is_admin"] = False + # Work with the object + ``` + patterns: + - pattern: "eval($X,...)" + - pattern-not: | + eval("...") + - pattern-not: | + eval("..." % <... "..." ...>) + - pattern-not: | + eval(<... "...".format( "..." ) ...>) + - pattern-not-inside: | + def eval(...): + ... + ... + metadata: + cwe: "CWE-95" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + category: "security" + shortDescription: "Improper neutralization of directives in dynamically evaluated + code ('Eval Injection')" + security-severity: "High" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/exec/rule-exec-used.yml b/data/rules/gitlab-sast-rules/python/exec/rule-exec-used.yml new file mode 100644 index 0000000..79e4e00 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/exec/rule-exec-used.yml @@ -0,0 +1,46 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_exec_rule-exec-used" + languages: + - "python" + message: | + The application was found calling the `exec` function with a non-literal variable. If the + variable comes from user-supplied input, an adversary could compromise the entire system by + executing arbitrary python code. + + To remediate this issue, remove all calls to `exec` and consider alternative methods for + executing the necessary business logic. There is almost no safe method of calling `eval` + with user-supplied input. + + If the application only needs to convert strings into objects, consider using `json.loads`. + In some cases `ast.literal_eval` is recommended, but this should be avoided as it can still + suffer from other issues such as the ability for malicious code to crash the python + interpreter or application. + + Example using `json.loads`` to load in arbitrary data to create data structures: + ``` + # User supplied data as a blob of JSON + user_supplied_data = """{"user": "test", "metadata": [1,2,3]}""" + # Load the JSON + user_object = json.loads(user_supplied_data) + # Manually add protected properties _after_ loading, never before + user_object["is_admin"] = False + # Work with the object + ``` + metadata: + cwe: "CWE-95" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + category: "security" + shortDescription: "Improper neutralization of directives in dynamically evaluated + code ('Eval Injection')" + security-severity: "High" + patterns: + - pattern: "exec(...)" + - pattern-not: "exec(\"...\")" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/exec/rule-linux-command-wildcard-injection.yml b/data/rules/gitlab-sast-rules/python/exec/rule-linux-command-wildcard-injection.yml new file mode 100644 index 0000000..aa774d8 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/exec/rule-linux-command-wildcard-injection.yml @@ -0,0 +1,52 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: + - id: "python_exec_rule-linux-command-wildcard-injection" + languages: + - "python" + message: | + Detected use of the wildcard character in a system call that spawns a shell. This subjects the + wildcard to normal shell expansion, which can have unintended consequences if there exist any + non-standard file names. For instance, a file named `-e sh script.sh` could cause issues when + expanded by the shell and executed as a command. Consider using a different method to achieve + the same result, such as using the `glob` module to expand the wildcard before passing it to the + system call. Or if the command is static, consider hardcoding the command instead of using a + wildcard. + + For example, the below code uses the glob module to expand the wildcard and get a list of all + CSV files in the current directory. This list is then used in the subprocess.run call, instead of a + wildcard. This avoids the potential issues that can arise from using a wildcard in a system call. + ``` + import glob + import subprocess + + # Secure way to process all CSV files + files = glob.glob('*.csv') + for file in files: + subprocess.run(['process_data', file]) + ``` + + metadata: + category: "security" + cwe: "CWE-155" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + shortDescription: "Improper neutralization of wildcards or matching symbols" + security-severity: "High" + patterns: + - pattern-either: + - pattern: |- + os.$X("$CMD", ...) + - pattern: |- + subprocess.Popen("$CMD", shell=True, ...) + - metavariable-regex: + metavariable: $CMD + regex: '(.*?)(\*|\?)' + - pattern-not-inside: |- + os.spawnvp(...) + metavariable-regex: + X: "(system|popen|popen2|popen3|popen4)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/exec/rule-os-path.yml b/data/rules/gitlab-sast-rules/python/exec/rule-os-path.yml new file mode 100644 index 0000000..0d4545e --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/exec/rule-os-path.yml @@ -0,0 +1,30 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_exec_rule-os-path" + languages: + - "python" + message: | + Starting a process with a shell; seems safe, but may be changed in the future, consider + rewriting without shell + pattern-either: + - pattern: "os.system(\"...\", ...)" + - pattern: "$OS.popen(\"...\", ...)" + - pattern: "$OS.popen2(\"...\", ...)" + - pattern: "$OS.popen3(\"...\", ...)" + - pattern: "$OS.popen4(\"...\", ...)" + - pattern: "commands.getoutput(\"...\", ...)" + - pattern: "commands.getstatusoutput(\"...\", ...)" + metadata: + cwe: "CWE-78" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of special elements used in an OS Command + ('OS Command Injection')" + security-severity: "High" + category: "security" + severity: "INFO" diff --git a/data/rules/gitlab-sast-rules/python/exec/rule-os-popen2.yml b/data/rules/gitlab-sast-rules/python/exec/rule-os-popen2.yml new file mode 100644 index 0000000..d0ba0c4 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/exec/rule-os-popen2.yml @@ -0,0 +1,36 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_exec_rule-os-popen2" + languages: + - "python" + message: | + Starting a process with a shell; seems safe, but may be changed in the future, consider + rewriting without shell + patterns: + - pattern-either: + - pattern: "os.system(...)" + - pattern: "os.popen(...)" + - pattern: "os.popen2(...)" + - pattern: "os.popen3(...)" + - pattern: "os.popen4(...)" + - pattern: "popen2.popen2(...)" + - pattern: "popen2.popen3(...)" + - pattern: "popen2.popen4(...)" + - pattern: "popen2.Popen3(...)" + - pattern: "popen2.Popen4(...)" + - pattern: "commands.getoutput(...)" + - pattern: "commands.getstatusoutput(\"\")" + metadata: + cwe: "CWE-78" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of special elements used in an OS Command + ('OS Command Injection')" + security-severity: "High" + category: "security" + severity: "INFO" diff --git a/data/rules/gitlab-sast-rules/python/exec/rule-start-process-with-no-shell.yml b/data/rules/gitlab-sast-rules/python/exec/rule-start-process-with-no-shell.yml new file mode 100644 index 0000000..f84f48e --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/exec/rule-start-process-with-no-shell.yml @@ -0,0 +1,47 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_exec_rule-start-process-with-no-shell" + languages: + - "python" + message: | + Found dynamic content when spawning a process. This is dangerous if externaldata can reach this + function call because it allows a malicious actor toexecute commands. Ensure no external data + reaches here. + patterns: + - pattern-either: + - patterns: + - pattern-not: "os.$W(\"...\", ...)" + - pattern-either: + - pattern: "os.execl(...)" + - pattern: "os.execle(...)" + - pattern: "os.execlp(...)" + - pattern: "os.execlpe(...)" + - pattern: "os.execv(...)" + - pattern: "os.execve(...)" + - pattern: "os.execvp(...)" + - pattern: "os.execvpe(...)" + - pattern: "os.startfile(...)" + - patterns: + - pattern-either: + - pattern: "os.spawnl(...)" + - pattern: "os.spawnle(...)" + - pattern: "os.spawnlp(...)" + - pattern: "os.spawnlpe(...)" + - pattern: "os.spawnv(...)" + - pattern: "os.spawnve(...)" + - pattern: "os.spawnvp(...)" + - pattern: "os.spawnvpe(...)" + metadata: + cwe: "CWE-78" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of special elements used in an OS Command + ('OS Command Injection')" + security-severity: "High" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-call.yml b/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-call.yml new file mode 100644 index 0000000..f80bea8 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-call.yml @@ -0,0 +1,43 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_exec_rule-subprocess-call" + languages: + - "python" + message: | + Python possesses many mechanisms to invoke an external executable. However, + doing so may present a security issue if appropriate care is not taken to + sanitize any user provided or variable input. This plugin test is part of a + family of tests built to check for process spawning and warn appropriately. + Specifically, this test looks for the spawning of a subprocess without the + use of a command shell. This type of subprocess invocation is not + vulnerable to shell injection attacks, but care should still be taken to + ensure validity of input. + patterns: + - pattern-not: "subprocess.$FUNC($ARG, shell=<... True ...>)" + - pattern-not: "subprocess.$FUNC($ARG, shell=<... 'True' ...>)" + - pattern-not: "subprocess.$FUNC($ARG, shell=<... \"True\" ...>)" + - pattern-either: + - pattern: | + subprocess.$FUNC($ARG, shell=False) + - pattern: | + subprocess.$FUNC($ARG, shell=0) + - pattern: | + subprocess.$FUNC($ARG, shell={...}) + - pattern: | + subprocess.$FUNC($ARG, shell=[...]) + - pattern: | + subprocess.$FUNC($ARG) + metadata: + cwe: "CWE-78" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of special elements used in an OS Command + ('OS Command Injection')" + security-severity: "High" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-popen-shell-true.yml b/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-popen-shell-true.yml new file mode 100644 index 0000000..d4dc537 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-popen-shell-true.yml @@ -0,0 +1,48 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_exec_rule-subprocess-popen-shell-true" + languages: + - "python" + patterns: + - patterns: + - pattern-not-inside: | + ... + $ARG = '...'.format('...') + ... + - pattern: "subprocess.$FUNC($ARG, ...)" + - pattern-not: "subprocess.$FUNC('...', ...)" + - pattern-not: "subprocess.$FUNC('...' % '...', ...)" + - pattern-not: "subprocess.$FUNC('...'.format('...'), ...)" + - pattern-either: + - pattern: "subprocess.$FUNC(..., shell=True, ...)" + - pattern: "subprocess.$FUNC(..., shell=[$V, ...], ...)" + - pattern: "subprocess.$FUNC(..., shell={$K: $V, ...}, ...)" + - patterns: + - pattern: "subprocess.$FUNC(..., shell=$INTVAL, ...)" + - pattern-not: "subprocess.$FUNC(..., shell=0, ...)" + - metavariable-regex: + metavariable: "$INTVAL" + regex: "^[0-9]+$" + - patterns: + - pattern: "subprocess.$FUNC(..., shell='$STRVAL', ...)" + - pattern-not: "subprocess.$FUNC(..., shell='', ...)" + message: | + Found `subprocess` function `$FUNC` with `shell=True`. This is dangerous because this call will + spawn the command using a shell process. Doing so propagates current shell settings and + variables, + which makes it much easier for a malicious actor to execute commands. Use `shell=False` + instead. + metadata: + cwe: "CWE-78" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of special elements used in an OS Command + ('OS Command Injection')" + security-severity: "High" + category: "security" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-shell-TRUE.yml b/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-shell-TRUE.yml new file mode 100644 index 0000000..2c7ec04 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/exec/rule-subprocess-shell-TRUE.yml @@ -0,0 +1,24 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_exec_rule-subprocess-shell-TRUE" + languages: + - "python" + message: | + subprocess call - check for execution of untrusted input + patterns: + - pattern-not: "subprocess.$FUNC(..., shell=True, ...)" + - pattern: "$FOO(..., shell=True, ...)" + metadata: + cwe: "CWE-78" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper neutralization of special elements used in an OS Command + ('OS Command Injection')" + security-severity: "High" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/file_permissions/rule-general-bad-permission.yml b/data/rules/gitlab-sast-rules/python/file_permissions/rule-general-bad-permission.yml new file mode 100644 index 0000000..dca0a2f --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/file_permissions/rule-general-bad-permission.yml @@ -0,0 +1,41 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_file-permissions_rule-general-bad-permission" + languages: + - "python" + message: | + The application was found setting file permissions to overly permissive values. Consider + using the following values if the application user is the only process to access + the file: + + - 0400 - read only access to the file + - 0200 - write only access to the file + - 0600 - read/write access to the file + + Example creating a file with read/write permissions for the application user: + ``` + # Use octal values to set 0o600 (read/write access to the file) for the current + # user + os.chmod('somefile.txt', 0o600) + ``` + + For all other values please see: + https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation + metadata: + shortDescription: "Incorrect permission assignment for critical resource" + cwe: "CWE-732" + category: "security" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + patterns: + - pattern: "os.chmod(...,$MASK)" + - metavariable-regex: + metavariable: "$MASK" + regex: "(0x..f|0o..[2,3,7]|stat.S_IXGRP|stat.S_IWOTH)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/files/rule-tarfile-unsafe-members.yml b/data/rules/gitlab-sast-rules/python/files/rule-tarfile-unsafe-members.yml new file mode 100644 index 0000000..be9b8ef --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/files/rule-tarfile-unsafe-members.yml @@ -0,0 +1,109 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_files_rule-tarfile-unsafe-members" + languages: + - "python" + message: | + The application may be vulnerable to a path traversal if it extracts untrusted archive files. + This vulnerability is colloquially known as 'Zip Slip'. Archive files may contain folders + which, + when extracted, may write outside of the intended directory. This is exploited by including + path traversal characters such as `../../other/directory` to overwrite or place files in system + or application directories. + + Extra care must be taken when extracting archive files as there are numerous concerns: + + - If possible, generate unique filenames instead of using the archives file names, as it may be + possible for users to overwrite files if the filenames are the same. + - Validate file paths are written with a prefixed, known trusted directory. + - Only process regular files and not symbolic links, as some applications may attempt to + read/follow + the symbolic link, leading to arbitrary file read / write vulnerabilities. + + Example of securely processing an archive file: + ``` + import tarfile + import uuid + # import os + + tar = tarfile.open('some.tar') + + # Max number of allowed files in our archive + max_files = 10 + # Max size for all files in archive + max_size = 1024 * 1024 * 10 # 10MB + # Max size per file in archive + max_file_size = 1024 * 1024 # 1MB + + # Validate number of files in archive + if len(tar.getmembers()) > max_files: + raise Exception("Too many files in archive") + + total_size = 0 + # Loop over all files to see if we exceed max size + # if so, do not process any of them. + for f in tar.getmembers(): + total_size += f.size + if total_size >= max_size: + raise Exception("Archive files exceeded max file size") + + # Iterate over files now that we know the total size is within limits + for f in tar.getmembers(): + # Internally this calls TarInfo.isreg() which ensures + # the file is a regular file and not a sym link or directory + if not f.isfile(): + continue + + # Optional, set a limit on each file size + if f.size > max_file_size: + raise Exception(f"File {f.name} too large: {f.size}") + + # If original names are required, ensure that only the + # filename is used: + # filename = os.path.basename(f.name) + + # More secure, generate a UUID4 value instead + filename = uuid.uuid4().hex + + # Reset the archive filename to the basename + # Newer versions of python (3.11.4+) should use: + # new_tar = old_tar.replace(name=...new name...) + f.name = filename + + # Extract the file into a restricted directory, with our + # own user's attributes, not the file from the archive + tar.extract(f, '/opt/app/restricted/', set_attrs=False) + ``` + + For more information on tarfile see: + - https://docs.python.org/3/library/tarfile.html + metadata: + cwe: "CWE-22" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + shortDescription: "Improper limitation of a pathname to a restricted directory + ('Path Traversal')" + security-severity: "Medium" + category: "security" + patterns: + - pattern-inside: | + import tarfile + ... + - pattern-either: + - patterns: + - pattern-inside: | + $TAR = tarfile.open(...) + ... + - pattern-either: + - pattern: "$TAR.extractall(...)" + - pattern: "tarfile.extractall(..., members=$TAR)" + - pattern: "$TAR.extractall(..., members=[])" + - patterns: + - pattern: "tarfile.extractall(...)" + - pattern: "tarfile.extractall(..., members=[])" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/flask/rule-app-debug.yml b/data/rules/gitlab-sast-rules/python/flask/rule-app-debug.yml new file mode 100644 index 0000000..8463dad --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/flask/rule-app-debug.yml @@ -0,0 +1,34 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_flask_rule-app-debug" + languages: + - "python" + message: | + The Flask application is running with `debug=True` configured. By enabling this option, certain + exceptions or errors could cause sensitive information to be leaked in HTTP responses. + + Additionally, it is not recommended to run a Flask application using `Flask.run(...)` in + production. Instead, a WSGI server such as + [gunicorn](https://flask.palletsprojects.com/en/2.3.x/deploying/gunicorn/) + or [waitress](https://flask.palletsprojects.com/en/2.3.x/deploying/waitress/) be used instead. + + For more information on deployment options for Flask applications see: + - https://flask.palletsprojects.com/en/2.3.x/deploying/ + metadata: + cwe: "CWE-489" + category: "security" + owasp: + - "A6:2017-Security Misconfiguration" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + shortDescription: "Active debug code" + security-severity: "Medium" + patterns: + - pattern-inside: | + import flask + ... + - pattern: "$APP.run(..., debug=True, ...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/ftp/rule-ftplib.yml b/data/rules/gitlab-sast-rules/python/ftp/rule-ftplib.yml new file mode 100644 index 0000000..a05d038 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/ftp/rule-ftplib.yml @@ -0,0 +1,55 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_ftp_rule-ftplib" + languages: + - "python" + message: | + The application was found using an FTP library. As FTP does not provide encryption, it is + strongly recommended that any file transfers be done over a more secure transport such as + SSH. + + The [paramiko](https://www.paramiko.org/) library can be used with an SCP module to allow + secure file transfers. + + Example using `paramiko` SSH client and the `scp` module: + ``` + import paramiko + import scp + + # Create an SSH client + with paramiko.SSHClient() as ssh: + # Load the system host keys so we can confirm the + # host we are connecting to is legitimate + ssh.load_system_host_keys('/home/appuser/.ssh/known_hosts') + + # Connect to the remote host using our SSH private key + ssh.connect(hostname='example.org', + port=22, + username='appuser', + key_filename='/home/appuser/.ssh/private_key') + + # Create an SCP client with the ssh transport and copy files + with scp.SCPClient(ssh.get_transport()) as secure_copy: + secure_copy.get('remote/test.file', 'local/test.file') + secure_copy.put('local/some.file', 'remote/some.file') + ``` + + For more information on the paramiko module see: + - https://www.paramiko.org/ + + For more information on the scp module see: + - https://github.com/jbardin/scp.py + metadata: + cwe: "CWE-319" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Cleartext transmission of sensitive information" + security-severity: "Medium" + pattern: "ftplib.$ANYTHING(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/log/rule-logging-config-insecure-listen.yml b/data/rules/gitlab-sast-rules/python/log/rule-logging-config-insecure-listen.yml new file mode 100644 index 0000000..066ebac --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/log/rule-logging-config-insecure-listen.yml @@ -0,0 +1,35 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_log_rule-logging-config-insecure-listen" + languages: + - "python" + message: | + The application was found calling the `logging.config.listen`` function, which provides the + ability to listen for + external configuration files over a socket server. This listen socket parses part of the + configuration and calls + `eval` on the supplied configuration file. A local user, or an adversary who is able to + exploit + a Server Side Request Forgery (SSRF) attack to communicate over localhost, would be able to + execute arbitrary + code by passing in a logging config that contains python code. + + To remediate the issue, remove the call to `logging.config.listen` method. + + For more information on the listen functionality see: + - https://docs.python.org/3/library/logging.config.html#logging.config.listen + metadata: + cwe: "CWE-94" + shortDescription: "Improper control of generation of code ('Code Injection')" + category: "security" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + security-severity: "High" + patterns: + - pattern: "logging.config.listen(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/random/rule-random.yml b/data/rules/gitlab-sast-rules/python/random/rule-random.yml new file mode 100644 index 0000000..d4d72ad --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/random/rule-random.yml @@ -0,0 +1,48 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_random_rule-random" + languages: + - "python" + message: | + Depending on the context, generating weak random numbers may expose cryptographic functions, + which rely on these numbers, to be exploitable. When generating numbers for sensitive values + such as tokens, nonces, and cryptographic keys, it is recommended that the `secrets` module + be used instead. + + Example using the secrets module: + ``` + import secrets + + # Generate a secure random 64 byte array + random_bytes = secrets.token_bytes(64) + print(random_bytes) + + # Generate a secure random 64 byte array as a hex string + random_bytes_hex = secrets.token_hex(64) + + # Generate a secure random 64 byte array base64 encoded for use in URLs + random_string = secrets.token_urlsafe(64) + ``` + + For more information on the `secrets` module see: + - https://docs.python.org/3/library/secrets.html + metadata: + cwe: "CWE-338" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + category: "security" + shortDescription: "Use of cryptographically weak pseudo-random number generator (PRNG)" + security-severity: "Low" + pattern-either: + - pattern: "random.random(...)" + - pattern: "random.randrange(...)" + - pattern: "random.randint(...)" + - pattern: "random.choice(...)" + - pattern: "random.uniform(...)" + - pattern: "random.triangular(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/requests/rule-request-without-timeout.yml b/data/rules/gitlab-sast-rules/python/requests/rule-request-without-timeout.yml new file mode 100644 index 0000000..6c633e3 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/requests/rule-request-without-timeout.yml @@ -0,0 +1,55 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_requests_rule-request-without-timeout" + languages: + - "python" + message: | + The application was found using the `requests` module without configuring a timeout value for + connections. This could lead to uncontrolled resource consumption where the application could + run out of + socket descriptors, effectively causing a Denial of Service (DoS). + + To remediate this issue, pass in a `timeout=` argument to each `requests` call. + + Example using a timeout for an HTTP GET request: + ``` + # Issue a GET request to https://example.com with a timeout of 10 seconds + response = requests.get('https://example.com', timeout=10) + # Work with the response object + # ... + ``` + + For more information on using the requests module see: + - https://requests.readthedocs.io/en/latest/api/ + patterns: + - pattern-either: + - patterns: + - pattern: "requests.$METHOD(..., timeout=$VAL, ...)" + - metavariable-comparison: + comparison: "$VAL <= 0" + metavariable: "$VAL" + - patterns: + - pattern: "requests.$METHOD(..., timeout=$VAL, ...)" + - metavariable-regex: + metavariable: "$VAL" + regex: "(^None)" + - patterns: + - pattern-not: "requests.$METHOD(..., timeout=$VAL, ...)" + - pattern-either: + - pattern: "requests.$METHOD(..., ...)" + - pattern: "requests.$METHOD(...)" + - metavariable-regex: + metavariable: "$METHOD" + regex: "(get|put|delete|post|options|head|patch)" + metadata: + cwe: "CWE-770" + category: "security" + shortDescription: "Allocation of resources without limits or throttling" + owasp: + - "A6:2017-Security Misconfiguration" + - "A05:2021-Security Misconfiguration" + security-severity: "Medium" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/snmp/rule-insecure-snmp-version.yml b/data/rules/gitlab-sast-rules/python/snmp/rule-insecure-snmp-version.yml new file mode 100644 index 0000000..e43d3d4 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/snmp/rule-insecure-snmp-version.yml @@ -0,0 +1,45 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_snmp_rule-insecure-snmp-version" + languages: + - "python" + message: | + Pysnmp was detected using versions SNMPv1 or SNMPv2. SNPMv1 and SNMPv2 are insecure + and should no longer be used as they do not offer encryption. + + If possible, query SNMP devices using SNMPv3 instead. + + Example querying a device using SNMPv3 with SHA-AES: + ``` + from pysnmp.hlapi import * + # Create the snpm iterator + iterator = getCmd( + SnmpEngine(), + # Configure using SHA AES + UsmUserData('usr-sha-aes', 'authkey1', 'privkey1', + authProtocol=USM_AUTH_HMAC96_SHA, + privProtocol=USM_PRIV_CFB128_AES), + UdpTransportTarget(('demo.snmplabs.com', 161)), + ContextData(), + ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)) + ) + ``` + + For more information on using SNMPv3 with `Pysnmp` see: + - https://pysnmp.readthedocs.io/en/latest/examples/hlapi/v3arch/asyncore/sync/manager/cmdgen/snmp-versions.html#snmpv3-auth-sha-privacy-aes128 + metadata: + cwe: "CWE-319" + category: "security" + shortDescription: "Cleartext transmission of sensitive information" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + pattern-either: + - pattern: "pysnmp.hlapi.CommunityData(..., mpModel=0, ...)" + - pattern: "pysnmp.hlapi.CommunityData(..., mpModel=1, ...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/snmp/rule-snmp-weak-cryptography.yml b/data/rules/gitlab-sast-rules/python/snmp/rule-snmp-weak-cryptography.yml new file mode 100644 index 0000000..0e8806c --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/snmp/rule-snmp-weak-cryptography.yml @@ -0,0 +1,67 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_snmp_rule-snmp-weak-cryptography" + languages: + - "python" + message: | + Pysnmp was detected using SNMPv3 without authentication or encryption + protections enabled. + + - Use of `usmNoAuthProtocol` or `usmNoPrivProtocol` indicates that + either authentication or privacy, respectively, is not being used. + - The absence of `authKey` (or `authKey=None`) implies no authentication, + which is equivalent to using `usmNoAuthProtocol`. + - The absence of `privKey` (or `privKey=None`) implies no privacy (encryption), + which is equivalent to using `usmNoPrivProtocol`. + + To enhance the security of your SNMP communications, it is recommended to use both + authentication and privacy features in SNMPv3: + + - Use SHA for Authentication: SHA (Secure Hash Algorithm) is a more secure option + for SNMP message authentication. To use SHA, set the `authProtocol` to + `usmHMACSHAAuthProtocol` and provide a strong `authKey`. + - Use AES for Privacy: AES (Advanced Encryption Standard) is recommended for + encrypting SNMP messages. Set the `privProtocol` to `usmAesCfb128Protocol` + or a similar AES-based protocol and specify a strong `privKey`. + + Example of secure `UsmUserData` configuration: + ``` + from pysnmp.hlapi import UsmUserData, usmHMACSHAAuthProtocol, usmAesCfb128Protocol + + user_data = UsmUserData('username','authKey', 'privKey', + authProtocol=usmHMACSHAAuthProtocol, + privProtocol=usmAesCfb128Protocol) + ``` + metadata: + cwe: "CWE-319" + category: "security" + shortDescription: "Cleartext transmission of sensitive information" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + security-severity: "Medium" + pattern-either: + - pattern-regex: "UsmUserData(.*usmNoAuthProtocol.*)" + - pattern-regex: "UsmUserData(.*usmNoPrivProtocol.*)" + - pattern: | + UsmUserData(..., authKey=None, ...) + - pattern: | + UsmUserData(..., privKey=None, ...) + - pattern: | + UsmUserData(..., authProtocol=(1,3,6,1,6,3,10,1,1,1), ...) + - pattern: | + UsmUserData(..., privProtocol=(1,3,6,1,6,3,10,1,2,1), ...) + - patterns: + - pattern-not: | + UsmUserData($NAME,$AUTHKEY,"...", ...) + - pattern-not: | + UsmUserData(..., privKey=$PRIVKEY, ...) + - pattern-not: | + UsmUserData(..., privProtocol=$PRIVPROT, ...) + - pattern: | + UsmUserData(...) + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/sql/rule-hardcoded-sql-expression.yml b/data/rules/gitlab-sast-rules/python/sql/rule-hardcoded-sql-expression.yml new file mode 100644 index 0000000..a964b3b --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/sql/rule-hardcoded-sql-expression.yml @@ -0,0 +1,86 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_sql_rule-hardcoded-sql-expression" + languages: + - "python" + message: | + SQL Injection is a critical vulnerability that can lead to data or system compromise. By + dynamically generating SQL query strings, user input may be able to influence the logic of + the SQL statement. This could lead to an adversary accessing information they should + not have access to, or in some circumstances, being able to execute OS functionality or code. + + Replace all dynamically generated SQL queries with parameterized queries. In situations where + dynamic queries must be created, never use direct user input, but instead use a map or + dictionary of valid values and resolve them using a user supplied key. + + For example, some database drivers do not allow parameterized queries for `>` or `<` comparison + operators. In these cases, do not use a user supplied `>` or `<` value, but rather have the + user + supply a `gt` or `lt` value. The alphabetical values are then used to look up the `>` and `<` + values to be used in the construction of the dynamic query. The same goes for other queries + where + column or table names are required but cannot be parameterized. + + Example using `PreparedStatement` queries: + ``` + import sqlite3 + + # Create a new database (in memory) + con = sqlite3.connect(":memory:") + # Get a cursor from the connection + cur = con.cursor() + # Create a tuple of the value to be used in the parameterized query + params = ('user-input',) + # execute the statement, passing in the params for the value + cur.execute("select name from sqlite_master where name = ?", params) + # work with the result + result = cur.fetchall() + ``` + + For more information on SQL Injection see OWASP: + https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html + metadata: + cwe: "CWE-89" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + category: "security" + shortDescription: "Improper neutralization of special elements used in an SQL + Command ('SQL Injection')" + security-severity: "High" + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern: | + "$QUERY" % ... + - pattern: | + "$QUERY".format(...) + - pattern: | + f"$QUERY{...}..." + - pattern: | + "$QUERY" + ... + - pattern: | + $TMP = "$QUERY" + ... + $SQL = $TMP + "..." % ... + - pattern: | + $SQL = "$QUERY" + ... + $SQL += ... + - metavariable-regex: + metavariable: $QUERY + regex: (?i)^(SELECT|INSERT|UPDATE|DELETE)\s + pattern-sinks: + - patterns: + - pattern: $DB.execute(...) + - pattern-not: + pattern-either: + - pattern: $DB.execute("...") + - pattern: $DB.execute("$QUERY" % "...") + - pattern: $DB.execute("$QUERY" + "...") + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/ssh/rule-ssh-nohost-key-verification.yml b/data/rules/gitlab-sast-rules/python/ssh/rule-ssh-nohost-key-verification.yml new file mode 100644 index 0000000..6dabedf --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/ssh/rule-ssh-nohost-key-verification.yml @@ -0,0 +1,57 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_ssh_rule-ssh-nohost-key-verification" + languages: + - "python" + message: | + The application was found to ignore host keys. Host keys are important as + they provide assurance that the client can prove that the host is trusted. + By ignoring these host keys, it is impossible for the client to validate the + connection is to a trusted host. + + To remediate this issue, remove the call to `set_missing_host_key_policy(...)` which + sets the host key policy. Instead, load key files using either `load_system_host_keys` + or `load_host_keys` to only allow known good hosts. By not setting a host key policy + for unknown hosts, `paramiko`'s default policy is to use `RejectPolicy`. + + Example configuration connecting to a known, trusted host, and not allowing connections + to unknown hosts: + ``` + import paramiko + + # Create an SSH client + with paramiko.SSHClient() as ssh: + # Load the system host keys so we can confirm the + # host we are connecting to is legitimate + ssh.load_system_host_keys('/home/appuser/.ssh/known_hosts') + + # Connect to the remote host using our SSH private key + ssh.connect(hostname='example.org', + port=22, + username='appuser', + key_filename='/home/appuser/.ssh/private_key') + ``` + + For more information on `set_missing_host_key_policy` see: + - https://docs.paramiko.org/en/stable/api/client.html#paramiko.client.SSHClient.set_missing_host_key_policy + metadata: + cwe: "CWE-322" + category: "security" + owasp: + - "A2:2017-Broken Authentication" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Key exchange without entity authentication" + security-severity: "Medium" + patterns: + - pattern-inside: | + $CLIENT = paramiko.client.SSHClient(...) + ... + $CLIENT.set_missing_host_key_policy(...) + - pattern-either: + - pattern: "paramiko.client.AutoAddPolicy" + - pattern: "paramiko.client.WarningPolicy" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/ssl/rule-req-no-certvalid.yml b/data/rules/gitlab-sast-rules/python/ssl/rule-req-no-certvalid.yml new file mode 100644 index 0000000..ac30a75 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/ssl/rule-req-no-certvalid.yml @@ -0,0 +1,51 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_ssl_rule-req-no-certvalid" + languages: + - "python" + message: | + The application was found using the `requests` module without configuring a timeout value for + connections. The `verify=False` argument has been set, which effectively disables the + validation + of server certificates. + + This allows for an adversary who is in between the application and the target host to intercept + potentially sensitive information or transmit malicious data. + + To remediate this issue either remove the `verify=False` argument, or set `verify=True`to each + `requests` call. + + Example verifying server certificates for an HTTP GET request: + ``` + # Issue a GET request to https://example.com with a timeout of 10 seconds and verify the + # server certificate explicitly. + response = requests.get('https://example.com', timeout=10, verify=True) + # Work with the response object + # ... + ``` + + For more information on using the requests module see: + - https://requests.readthedocs.io/en/latest/api/ + metadata: + cwe: "CWE-295" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A07:2021-Identification and Authentication Failures" + - "A07:2025-Authentication Failures" + category: "security" + shortDescription: "Improper certificate validation" + security-severity: "Medium" + patterns: + - pattern-either: + - pattern: "requests.put(..., verify=False, ...)" + - pattern: "requests.patch(..., verify=False, ...)" + - pattern: "requests.delete(..., verify=False, ...)" + - pattern: "requests.head(..., verify=False, ...)" + - pattern: "requests.options(..., verify=False, ...)" + - pattern: "requests.request(..., verify=False, ...)" + - pattern: "requests.get(..., verify=False, ...)" + - pattern: "requests.post(..., verify=False, ...)" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/ssl/rule-ssl-no-version.yml b/data/rules/gitlab-sast-rules/python/ssl/rule-ssl-no-version.yml new file mode 100644 index 0000000..9965f8a --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/ssl/rule-ssl-no-version.yml @@ -0,0 +1,64 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_ssl_rule-ssl-no-version" + languages: + - "python" + message: | + The application was found calling `ssl.wrap_socket` without a TLS protocol version specified. + Additionally, `ssl.wrap_socket` has been deprecated since Python 3.7. It is strongly + recommended + that newer applications use TLS 1.2 or 1.3 and `SSLContext.wrap_socket`. + + To remediate this issue, create a new TLS context and pass in `ssl.PROTOCOL_TLS_CLIENT` + for clients or `ssl.PROTOCOL_TLS_SERVER` for servers to the `ssl.SSLContext(...)` `protocol=` + argument. When converting the socket to a TLS socket, use the new `SSLContext.wrap_socket` + method instead. + + + Example creating a TLS 1.3 client socket connection by using a newer version of Python + (3.11.4) and + the SSL module: + ``` + import ssl + import socket + + # Create our initial socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + # Connect the socket + sock.connect(('www.example.org', 443)) + + # Create a new SSLContext with protocol set to ssl.PROTOCOL_TLS_CLIENT + # This will auto-select the highest grade TLS protocol version (1.3) + context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) + # Load our a certificates for server certificate authentication + context.load_verify_locations('cert.pem') + # Create our TLS socket, and validate the server hostname matches + with context.wrap_socket(sock, server_hostname="www.example.org") as tls_sock: + # Send some bytes over the socket (HTTP request in this case)\ + data = bytes('GET / HTTP/1.1\r\nHost: example.org\r\n\r\n', 'utf-8') + sent_bytes = tls_sock.send(data) + # Validate number of sent bytes + # ... + # Read the response + resp = tls_sock.recv() + # Work with the response + # ... + ``` + + For more information on the ssl module see: + - https://docs.python.org/3/library/ssl.html + metadata: + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Inadequate encryption strength" + security-severity: "Medium" + category: "security" + patterns: + - pattern: "ssl.wrap_socket()" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/ssl/rule-ssl-with-bad-version.yml b/data/rules/gitlab-sast-rules/python/ssl/rule-ssl-with-bad-version.yml new file mode 100644 index 0000000..34dbe9c --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/ssl/rule-ssl-with-bad-version.yml @@ -0,0 +1,81 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_ssl_rule-ssl-with-bad-version" + languages: + - "python" + message: | + The application was found calling an SSL module with SSL or TLS protocols that have known + deficiencies. + It is strongly recommended that newer applications use TLS 1.2 or 1.3 and + `SSLContext.wrap_socket`. + + If using the `pyOpenSSL` module, please note that it has been deprecated and the Python + Cryptographic Authority + strongly suggests moving to use the [pyca/cryptography](https://github.com/pyca/cryptography) + module instead. + + To remediate this issue for the `ssl` module, create a new TLS context and pass in + `ssl.PROTOCOL_TLS_CLIENT` for clients or `ssl.PROTOCOL_TLS_SERVER` for servers to the + `ssl.SSLContext(...)` `protocol=` + argument. When converting the socket to a TLS socket, use the new `SSLContext.wrap_socket` + method instead. + + Example creating a TLS 1.3 client socket connection by using a newer version of Python + (3.11.4) and + the SSL module: + ``` + import ssl + import socket + + # Create our initial socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + # Connect the socket + sock.connect(('www.example.org', 443)) + + # Create a new SSLContext with protocol set to ssl.PROTOCOL_TLS_CLIENT + # This will auto-select the highest grade TLS protocol version (1.3) + context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) + # Load our a certificates for server certificate authentication + context.load_verify_locations('cert.pem') + # Create our TLS socket, and validate the server hostname matches + with context.wrap_socket(sock, server_hostname="www.example.org") as tls_sock: + # Send some bytes over the socket (HTTP request in this case)\ + data = bytes('GET / HTTP/1.1\r\nHost: example.org\r\n\r\n', 'utf-8') + sent_bytes = tls_sock.send(data) + # Validate number of sent bytes + # ... + # Read the response + resp = tls_sock.recv() + # Work with the response + # ... + ``` + + For more information on the ssl module see: + - https://docs.python.org/3/library/ssl.html + + For more information on pyca/cryptography and openssl see: + - https://cryptography.io/en/latest/openssl/ + metadata: + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Inadequate Encryption Strength" + security-severity: "Medium" + category: "security" + patterns: + - pattern-either: + - pattern: "ssl.PROTOCOL_SSLv2" + - pattern: "ssl.PROTOCOL_SSLv3" + - pattern: "ssl.PROTOCOL_TLSv1" + - pattern: "ssl.PROTOCOL_TLSv1_1" + - pattern: "pyOpenSSL.SSL.SSLv2_METHOD" + - pattern: "pyOpenSSL.SSL.SSLv23_METHOD" + - pattern: "pyOpenSSL.SSL.SSLv3_METHOD" + - pattern: "pyOpenSSL.SSL.TLSv1_METHOD" + - pattern: "pyOpenSSL.SSL.TLSv1_1_METHOD" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/ssl/rule-unverified-context.yml b/data/rules/gitlab-sast-rules/python/ssl/rule-unverified-context.yml new file mode 100644 index 0000000..c7fc63f --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/ssl/rule-unverified-context.yml @@ -0,0 +1,62 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_ssl_rule-unverified-context" + languages: + - "python" + message: | + The application was found creating a SSL context using the `_create_unverified_context`. + This effectively disables the validation of server certificates. + + This allows for an adversary who is in between the application and the target host to intercept + potentially sensitive information or transmit malicious data. + + To remediate this issue remove the call to `_create_unverified_context` and either create a + default + context using `ssl.create_default_context` or create a context with TLS 1.3. + + Example creating a TLS 1.3 client socket connection by using a newer version of Python + (3.11.4) and + the SSL module: + ``` + import ssl + import socket + + # Create our initial socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + # Connect the socket + sock.connect(('www.example.org', 443)) + + # Create a new SSLContext with protocol set to ssl.PROTOCOL_TLS_CLIENT + # This will auto-select the highest grade TLS protocol version (1.3) + context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) + # Load our a certificates for server certificate authentication + context.load_verify_locations('cert.pem') + # Create our TLS socket, and validate the server hostname matches + with context.wrap_socket(sock, server_hostname="www.example.org") as tls_sock: + # Send some bytes over the socket (HTTP request in this case)\ + data = bytes('GET / HTTP/1.1\r\nHost: example.org\r\n\r\n', 'utf-8') + sent_bytes = tls_sock.send(data) + # Validate number of sent bytes + # ... + # Read the response + resp = tls_sock.recv() + # Work with the response + # ... + ``` + + Unverified SSL context detected. This will permit insecure connections without `verifyingSSL` + certificates. Use `ssl.create_default_context()` instead. + metadata: + cwe: "CWE-295" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A07:2021-Identification and Authentication Failures" + - "A07:2025-Authentication Failures" + category: "security" + shortDescription: "Improper certificate validation" + security-severity: "Medium" + pattern: "ssl._create_unverified_context(...)" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/telnet/rule-import-telnib.yml b/data/rules/gitlab-sast-rules/python/telnet/rule-import-telnib.yml new file mode 100644 index 0000000..7cb11fb --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/telnet/rule-import-telnib.yml @@ -0,0 +1,48 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_telnet_rule-import-telnib" + languages: + - "python" + message: | + The application was found using a telnet library. As telnet does not provide encryption, it is + strongly recommended that communications use a more secure transport such as + SSH. + + The [paramiko](https://www.paramiko.org/) library can be used to initiate SSH connections. + + Example using `paramiko` SSH client: + ``` + import paramiko + import scp + + # Create an SSH client + with paramiko.SSHClient() as ssh: + # Load the system host keys so we can confirm the + # host we are connecting to is legitimate + ssh.load_system_host_keys('/home/appuser/.ssh/known_hosts') + + # Connect to the remote host using our SSH private key + ssh.connect(hostname='example.org', + port=22, + username='appuser', + key_filename='/home/appuser/.ssh/private_key') + # Work with the connection + ``` + + For more information on the paramiko module see: + - https://www.paramiko.org/ + metadata: + cwe: "CWE-319" + category: "security" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Cleartext transmission of sensitive information" + security-severity: "Medium" + patterns: + - pattern: "import telnetlib" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/tmpdir/rule-hardcodedtmp.yml b/data/rules/gitlab-sast-rules/python/tmpdir/rule-hardcodedtmp.yml new file mode 100644 index 0000000..0a3e6a7 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/tmpdir/rule-hardcodedtmp.yml @@ -0,0 +1,43 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_tmpdir_rule-hardcodedtmp" + languages: + - "python" + message: | + The application was found creating files in shared system temporary directories + (`/tmp` or `/var/tmp`) without using the `tempfile.TemporaryFile` function. Depending + on how the application uses this temporary file, an attacker may be able to create + symlinks that point to other files prior to the application creating or writing + to the target file, leading to unintended files being created or overwritten. + + Example using `tempfile.TemporaryFile` to write a file: + ``` + import tempfile + + # Open a new temporary file using a context manager + with tempfile.TemporaryFile() as fp: + # Write some data to the temporary file + fp.write(b'Some data') + # Seek back to beginning of file + fp.seek(0) + # Read it + data = fp.read() + # File is automatically closed/removed once we exit the with context + ``` + + For more information on alternative tempfile functions see: + - https://docs.python.org/3/library/tempfile.html + metadata: + cwe: "CWE-377" + category: "security" + shortDescription: "Insecure temporary file" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + security-severity: "Medium" + pattern: "$CALL(\"=~/^\\/tmp.*/\", ...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/tmpdir/rule-mktemp-q.yml b/data/rules/gitlab-sast-rules/python/tmpdir/rule-mktemp-q.yml new file mode 100644 index 0000000..12171fe --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/tmpdir/rule-mktemp-q.yml @@ -0,0 +1,44 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_tmpdir_rule-mktemp-q" + languages: + - "python" + message: | + The application was found creating temporary files with the insecure `mktemp` method. + Depending on how the application uses this temporary file, an attacker may be able to create + symlinks that point to other files prior to the application creating or writing + to the target file, leading to unintended files being created or overwritten. + + To remediate this issue consider using `tempfile.TemporaryFile` instead. + + Example using `tempfile.TemporaryFile` to write a file: + ``` + import tempfile + + # Open a new temporary file using a context manager + with tempfile.TemporaryFile() as fp: + # Write some data to the temporary file + fp.write(b'Some data') + # Seek back to beginning of file + fp.seek(0) + # Read it + data = fp.read() + # File is automatically closed/removed once we exit the with context + ``` + + For more information on alternative tempfile functions see: + - https://docs.python.org/3/library/tempfile.html + metadata: + cwe: "CWE-377" + category: "security" + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + shortDescription: "Insecure temporary file" + security-severity: "Medium" + pattern: "tempfile.mktemp(...)" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/python/urlopen/rule-urllib-urlopen.yml b/data/rules/gitlab-sast-rules/python/urlopen/rule-urllib-urlopen.yml new file mode 100644 index 0000000..b7ba020 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/urlopen/rule-urllib-urlopen.yml @@ -0,0 +1,66 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_urlopen_rule-urllib-urlopen" + languages: + - "python" + message: | + The application was found passing in a non-literal value to the `urllib` methods which issue + requests. `urllib` supports the `file://` scheme, which may allow an adversary who can control + the URL value to read arbitrary files on the file system. + + To remediate this issue either hardcode the URLs being used in urllib or use the `requests` + module instead. + + Example using the `requests` module to issue an HTTPS request: + ``` + import requests + # Issue a GET request to https://example.com with a timeout of 10 seconds + response = requests.get('https://example.com', timeout=10) + # Work with the response object + # ... + ``` + patterns: + - pattern-either: + - patterns: + - pattern-either: + - pattern: "urllib.$METHOD(...)" + - pattern: "urllib.request.$METHOD(...)" + - pattern-not: "urllib.$METHOD(\"...\")" + - pattern-not: "urllib.request.$METHOD(\"...\")" + - pattern-not: "urllib.$METHOD(\"...\", ...)" + - pattern-not: "urllib.request.$METHOD(\"...\", ...)" + - metavariable-regex: + metavariable: "$METHOD" + regex: "(urlopen|urlretrieve)" + - patterns: + - pattern-either: + - pattern-inside: | + $OPENER = urllib.URLopener(...) + ... + - pattern-inside: | + $OPENER = urllib.request.URLopener(...) + ... + - pattern-inside: | + $OPENER = urllib.FancyURLopener(...) + ... + - pattern-inside: | + $OPENER = urllib.request.FancyURLopener(...) + ... + - pattern-either: + - pattern: "$OPENER.open(...)" + - pattern: "$OPENER.retrieve(...)" + - pattern-not: "$OPENER.open(\"...\")" + - pattern-not: "$OPENER.retrieve(\"...\")" + metadata: + cwe: "CWE-918" + owasp: + - "A1:2017-Injection" + - "A10:2021-Server-Side Request Forgery" + - "A01:2025-Broken Access Control" + shortDescription: "Server-side request forgery (SSRF)" + security-severity: "Medium" + category: "security" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/xml/rule-celement.yml b/data/rules/gitlab-sast-rules/python/xml/rule-celement.yml new file mode 100644 index 0000000..a49efd9 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/xml/rule-celement.yml @@ -0,0 +1,59 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_xml_rule-celement" + languages: + - "python" + message: | + The application was found using the `xml.etree` package for processing XML. + Pythons default xml processors suffer from various XML parsing vulnerabilities + and care must be taken when handling XML data. Additionally, depending on the + version of Python, more critical vulnerabilities such as eXternal XML Entity + injection maybe exploitable. + + The `etree` package suffers from the following security risks as of Python 3.7.1: + * Billion laughs / exponential entity expansion - May allow an adversary to cause + a Denial of Service (DoS) against the application parsing arbitrary XML. + * Quadratic blowup entity expansion - Similar to above, but requires a larger input + to cause the Denial of Service. + + To remediate the above issues, consider using the + [defusedxml](https://pypi.org/project/defusedxml/) + library when processing untrusted XML. + + Example parsing an XML document using defusedxml: + ``` + from defusedxml.ElementTree import parse + + # Parse the inventory.xml file + et = parse('inventory.xml') + # Get the root element + root = et.getroot() + # Work with the root element + # ... + ``` + + For more information on the various XML parsers and their vulnerabilities please see: + - https://docs.python.org/3/library/xml.html#xml-vulnerabilities + + For more information on XML security see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python + metadata: + cwe: "CWE-611" + owasp: + - "A4:2017-XML External Entities (XXE)" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + category: "security" + shortDescription: "Improper restriction of XML external entity reference" + security-severity: "Medium" + pattern-either: + - patterns: + - pattern: "xml.etree.cElementTree.fromstring(...)" + - pattern-not: "xml.etree.cElementTree.fromstring(\"...\")" + - pattern: "xml.etree.cElementTree.parse(...)" + - pattern: "xml.etree.cElementTree.iterparse(...)" + - pattern: "xml.etree.cElementTree.XMLParser(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/xml/rule-element.yml b/data/rules/gitlab-sast-rules/python/xml/rule-element.yml new file mode 100644 index 0000000..0ceca8f --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/xml/rule-element.yml @@ -0,0 +1,59 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_xml_rule-element" + languages: + - "python" + message: | + The application was found using the `xml.etree` package for processing XML. + Pythons default xml processors suffer from various XML parsing vulnerabilities + and care must be taken when handling XML data. Additionally, depending on the + version of Python, more critical vulnerabilities such as eXternal XML Entity + injection maybe exploitable. + + The `etree` package suffers from the following security risks as of Python 3.7.1: + * Billion laughs / exponential entity expansion - May allow an adversary to cause + a Denial of Service (DoS) against the application parsing arbitrary XML. + * Quadratic blowup entity expansion - Similar to above, but requires a larger input + to cause the Denial of Service. + + To remediate the above issues, consider using the + [defusedxml](https://pypi.org/project/defusedxml/) + library when processing untrusted XML. + + Example parsing an XML document using defusedxml: + ``` + from defusedxml.ElementTree import parse + + # Parse the inventory.xml file + et = parse('inventory.xml') + # Get the root element + root = et.getroot() + # Work with the root element + # ... + ``` + + For more information on the various XML parsers and their vulnerabilities please see: + - https://docs.python.org/3/library/xml.html#xml-vulnerabilities + + For more information on XML security see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python + metadata: + cwe: "CWE-611" + owasp: + - "A4:2017-XML External Entities (XXE)" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + category: "security" + shortDescription: "Improper restriction of XML external entity reference" + security-severity: "Medium" + pattern-either: + - patterns: + - pattern: "xml.etree.ElementTree.fromstring(...)" + - pattern-not: "xml.etree.ElementTree.fromstring(\"...\")" + - pattern: "xml.etree.ElementTree.parse(...)" + - pattern: "xml.etree.ElementTree.iterparse(...)" + - pattern: "xml.etree.ElementTree.XMLParser(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/xml/rule-etree.yml b/data/rules/gitlab-sast-rules/python/xml/rule-etree.yml new file mode 100644 index 0000000..13a9f93 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/xml/rule-etree.yml @@ -0,0 +1,61 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_xml_rule-etree" + languages: + - "python" + message: | + The application was found using the `lxml.etree` package for processing XML. + Python's default XML processors suffer from various XML parsing vulnerabilities + and care must be taken when handling XML data. Additionally, depending on the + version of Python, more critical vulnerabilities such as eXternal XML Entity + injection maybe exploitable. + + The `etree` package suffers from the following security risks as of Python 3.7.1: + * Billion laughs / exponential entity expansion - May allow an adversary to cause + a Denial of Service (DoS) against the application parsing arbitrary XML. + * Quadratic blowup entity expansion - Similar to above, but requires a larger input + to cause the Denial of Service. + + To remediate the above issues, consider using the + [defusedxml](https://pypi.org/project/defusedxml/) + library when processing untrusted XML. + + Example parsing an XML document using defusedxml: + ``` + from defusedxml.ElementTree import parse + + # Parse the inventory.xml file + et = parse('inventory.xml') + # Get the root element + root = et.getroot() + # Work with the root element + # ... + ``` + + For more information on the various XML parsers and their vulnerabilities please see: + - https://docs.python.org/3/library/xml.html#xml-vulnerabilities + + For more information on XML security see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python + metadata: + cwe: "CWE-611" + owasp: + - "A4:2017-XML External Entities (XXE)" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + category: "security" + shortDescription: "Improper restriction of XML external entity reference" + security-severity: "Medium" + pattern-either: + - pattern: "lxml.etree.parse(...)" + - patterns: + - pattern: "lxml.etree.fromstring(...)" + - pattern-not: "lxml.etree.fromstring(\"...\")" + - pattern: "lxml.etree.RestrictedElement(...)" + - pattern: "lxml.etree.GlobalParserTLS(...)" + - pattern: "lxml.etree.getDefaultParser(...)" + - pattern: "lxml.etree.check_docinfo(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/xml/rule-expatbuilder.yml b/data/rules/gitlab-sast-rules/python/xml/rule-expatbuilder.yml new file mode 100644 index 0000000..1999d2b --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/xml/rule-expatbuilder.yml @@ -0,0 +1,58 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_xml_rule-expatbuilder" + languages: + - "python" + message: | + The application was found using the `xml.dom.expatbuilder` which calls the `xml.dom.minidom` + package for processing XML. Python's default XML processors suffer from various XML parsing + vulnerabilities + and care must be taken when handling XML data. Additionally, depending on the + version of Python, more critical vulnerabilities such as eXternal XML Entity + injection maybe exploitable. + + The `xml.dom.minidom` package suffers from the following security risks as of Python 3.7.1: + * Billion laughs / exponential entity expansion - May allow an adversary to cause + a Denial of Service (DoS) against the application parsing arbitrary XML. + * Quadratic blowup entity expansion - Similar to above, but requires a larger input + to cause the Denial of Service. + + To remediate the above issues, consider using the + [defusedxml](https://pypi.org/project/defusedxml/) + library when processing untrusted XML. + + Example parsing an XML document using defusedxml: + ``` + from defusedxml.ElementTree import parse + + # Parse the inventory.xml file + et = parse('inventory.xml') + # Get the root element + root = et.getroot() + # Work with the root element + # ... + ``` + + For more information on the various XML parsers and their vulnerabilities please see: + - https://docs.python.org/3/library/xml.html#xml-vulnerabilities + + For more information on XML security see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python + metadata: + cwe: "CWE-611" + owasp: + - "A4:2017-XML External Entities (XXE)" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + category: "security" + shortDescription: "Improper restriction of XML external entity reference" + security-severity: "Medium" + pattern-either: + - patterns: + - pattern: "xml.dom.expatbuilder.parse(...)" + - pattern-not: "xml.dom.expatbuilder.parse(\"...\")" + - pattern: "xml.dom.expatbuilder.parseString(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/xml/rule-expatreader.yml b/data/rules/gitlab-sast-rules/python/xml/rule-expatreader.yml new file mode 100644 index 0000000..0704220 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/xml/rule-expatreader.yml @@ -0,0 +1,59 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_xml_rule-expatreader" + languages: + - "python" + message: | + The application was found using the `xml.sax.expatreader` package for processing XML. Python's + default XML processors suffer from various XML parsing vulnerabilities + and care must be taken when handling XML data. Additionally, depending on the + version of Python, more critical vulnerabilities such as eXternal XML Entity + injection maybe exploitable. + + The `xml.sax` package suffers from the following security risks as of Python 3.7.1: + * Billion laughs / exponential entity expansion - May allow an adversary to cause + a Denial of Service (DoS) against the application parsing arbitrary XML. + * Quadratic blowup entity expansion - Similar to above, but requires a larger input + to cause the Denial of Service. + + To remediate the above issues, consider using the + [defusedxml](https://pypi.org/project/defusedxml/) + library when processing untrusted XML. + + Example parsing an XML document using defusedxml: + ``` + from defusedxml.ElementTree import parse + + # Parse the inventory.xml file + et = parse('inventory.xml') + # Get the root element + root = et.getroot() + # Work with the root element + # ... + ``` + + For more information on the various XML parsers and their vulnerabilities please see: + - https://docs.python.org/3/library/xml.html#xml-vulnerabilities + + For more information on XML security see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python + metadata: + cwe: "CWE-611" + owasp: + - "A4:2017-XML External Entities (XXE)" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + category: "security" + shortDescription: "Improper restriction of XML external entity reference" + security-severity: "Medium" + pattern-either: + - pattern: "xml.dom.expatreader.parse(...)" + - patterns: + - pattern: "xml.dom.expatreader.parseString(...)" + - pattern-not: "xml.dom.expatreader.parseString(\"...\")" + - pattern: "xml.dom.expatreader.parseString(...)" + - pattern: "xml.dom.expatreader.create_parser(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/xml/rule-minidom.yml b/data/rules/gitlab-sast-rules/python/xml/rule-minidom.yml new file mode 100644 index 0000000..f9aadc5 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/xml/rule-minidom.yml @@ -0,0 +1,57 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_xml_rule-minidom" + languages: + - "python" + message: | + The application was found using the `xml.dom.minidom` package for processing XML. Python's + default XML processors suffer from various XML parsing vulnerabilities + and care must be taken when handling XML data. Additionally, depending on the + version of Python, more critical vulnerabilities such as eXternal XML Entity + injection maybe exploitable. + + The `xml.dom.minidom` package suffers from the following security risks as of Python 3.7.1: + * Billion laughs / exponential entity expansion - May allow an adversary to cause + a Denial of Service (DoS) against the application parsing arbitrary XML. + * Quadratic blowup entity expansion - Similar to above, but requires a larger input + to cause the Denial of Service. + + To remediate the above issues, consider using the + [defusedxml](https://pypi.org/project/defusedxml/) + library when processing untrusted XML. + + Example parsing an XML document using defusedxml: + ``` + from defusedxml.ElementTree import parse + + # Parse the inventory.xml file + et = parse('inventory.xml') + # Get the root element + root = et.getroot() + # Work with the root element + # ... + ``` + + For more information on the various XML parsers and their vulnerabilities please see: + - https://docs.python.org/3/library/xml.html#xml-vulnerabilities + + For more information on XML security see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python + metadata: + cwe: "CWE-611" + owasp: + - "A4:2017-XML External Entities (XXE)" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + category: "security" + shortDescription: "Improper restriction of XML external entity reference" + security-severity: "Medium" + pattern-either: + - patterns: + - pattern: "xml.dom.minidom.parseString(...)" + - pattern-not: "xml.dom.minidom.parseString(\"...\")" + - pattern: "xml.dom.minidom.parse(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/xml/rule-pulldom.yml b/data/rules/gitlab-sast-rules/python/xml/rule-pulldom.yml new file mode 100644 index 0000000..bdfa3da --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/xml/rule-pulldom.yml @@ -0,0 +1,57 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_xml_rule-pulldom" + languages: + - "python" + message: | + The application was found using the `xml.dom.pulldom` package for processing XML. Python's + default XML processors suffer from various XML parsing vulnerabilities + and care must be taken when handling XML data. Additionally, depending on the + version of Python, more critical vulnerabilities such as eXternal XML Entity + injection maybe exploitable. + + The `xml.dom.pulldom` package suffers from the following security risks as of Python 3.7.1: + * Billion laughs / exponential entity expansion - May allow an adversary to cause + a Denial of Service (DoS) against the application parsing arbitrary XML. + * Quadratic blowup entity expansion - Similar to above, but requires a larger input + to cause the Denial of Service. + + To remediate the above issues, consider using the + [defusedxml](https://pypi.org/project/defusedxml/) + library when processing untrusted XML. + + Example parsing an XML document using defusedxml: + ``` + from defusedxml.ElementTree import parse + + # Parse the inventory.xml file + et = parse('inventory.xml') + # Get the root element + root = et.getroot() + # Work with the root element + # ... + ``` + + For more information on the various XML parsers and their vulnerabilities please see: + - https://docs.python.org/3/library/xml.html#xml-vulnerabilities + + For more information on XML security see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python + metadata: + cwe: "CWE-611" + owasp: + - "A4:2017-XML External Entities (XXE)" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + category: "security" + shortDescription: "Improper restriction of XML external entity reference" + security-severity: "Medium" + pattern-either: + - patterns: + - pattern: "xml.dom.pulldom.parseString(...)" + - pattern-not: "xml.dom.pulldom.parseString(\"...\")" + - pattern: "xml.dom.pulldom.parse(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/python/xml/rule-sax.yml b/data/rules/gitlab-sast-rules/python/xml/rule-sax.yml new file mode 100644 index 0000000..009fb29 --- /dev/null +++ b/data/rules/gitlab-sast-rules/python/xml/rule-sax.yml @@ -0,0 +1,58 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "python_xml_rule-sax" + languages: + - "python" + message: | + The application was found using the `xml.sax` package for processing XML. + Python's default XML processors suffer from various XML parsing vulnerabilities + and care must be taken when handling XML data. Additionally, depending on the + version of Python, more critical vulnerabilities such as eXternal XML Entity + injection maybe exploitable. + + The `xml.sax` package suffers from the following security risks as of Python 3.7.1: + * Billion laughs / exponential entity expansion - May allow an adversary to cause + a Denial of Service (DoS) against the application parsing arbitrary XML. + * Quadratic blowup entity expansion - Similar to above, but requires a larger input + to cause the Denial of Service. + + To remediate the above issues, consider using the + [defusedxml](https://pypi.org/project/defusedxml/) + library when processing untrusted XML. + + Example parsing an XML document using defusedxml: + ``` + from defusedxml.ElementTree import parse + + # Parse the inventory.xml file + et = parse('inventory.xml') + # Get the root element + root = et.getroot() + # Work with the root element + # ... + ``` + + For more information on the various XML parsers and their vulnerabilities please see: + - https://docs.python.org/3/library/xml.html#xml-vulnerabilities + + For more information on XML security see OWASP's guide: + - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python + metadata: + cwe: "CWE-611" + owasp: + - "A4:2017-XML External Entities (XXE)" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + category: "security" + shortDescription: "Improper restriction of XML external entity reference" + security-severity: "Medium" + pattern-either: + - pattern: "xml.sax.parse(...)" + - patterns: + - pattern: "xml.sax.parseString(...)" + - pattern-not: "xml.sax.parseString(\"...\")" + - pattern: "xml.sax.make_parser(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/scala/cookie/rule-RequestParamToCookie.yml b/data/rules/gitlab-sast-rules/scala/cookie/rule-RequestParamToCookie.yml new file mode 100644 index 0000000..70f5a5a --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/cookie/rule-RequestParamToCookie.yml @@ -0,0 +1,48 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_cookie_rule-RequestParamToCookie" + languages: + - "scala" + message: | + This code constructs an HTTP Cookie using an untrusted HTTP parameter. If this cookie is added + to an HTTP response, it will allow a HTTP response splitting vulnerability. See + http://en.wikipedia.org/wiki/HTTP_response_splitting for more information. + metadata: + category: "security" + cwe: "CWE-113" + owasp: + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP + Response Splitting')" + technology: + - "scala" + security-severity: "Info" + mode: "taint" + pattern-sanitizers: + - patterns: + - pattern-inside: |- + $STR.replaceAll("$REPLACE_CHAR", "$REPLACE"); + - pattern: "$STR" + - metavariable-regex: + metavariable: "$REPLACE_CHAR" + regex: "(.*\\\\r\\\\n.*)" + - metavariable-regex: + metavariable: "$REPLACE" + regex: "(?!(\\\\r\\\\n))" + - pattern: "org.owasp.encoder.Encode.forUriComponent(...)" + - pattern: "org.owasp.encoder.Encode.forUri(...)" + - pattern: "java.net.URLEncoder.encode(..., $CHARSET)" + pattern-sinks: + - pattern: "new javax.servlet.http.Cookie(\"$KEY\", ...);" + - patterns: + - pattern-inside: | + $C = new javax.servlet.http.Cookie("$KEY", ...); + ... + - pattern: "$C.setValue(...);" + pattern-sources: + - pattern: "($REQ: HttpServletRequest).getParameter(...);" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/cookie/rule-RequestParamToHeader.yml b/data/rules/gitlab-sast-rules/scala/cookie/rule-RequestParamToHeader.yml new file mode 100644 index 0000000..2b20048 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/cookie/rule-RequestParamToHeader.yml @@ -0,0 +1,46 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_cookie_rule-RequestParamToHeader" + languages: + - "scala" + message: | + This code directly writes an HTTP parameter to an HTTP header, which allows for a HTTP + response splitting vulnerability. See http://en.wikipedia.org/wiki/HTTP_response_splitting for + more information. + metadata: + category: "security" + cwe: "CWE-113" + owasp: + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP + Response Splitting')" + technology: + - "scala" + security-severity: "High" + mode: "taint" + pattern-sanitizers: + - patterns: + - pattern-inside: |- + $STR.replaceAll("$REPLACE_CHAR", "$REPLACE"); + - pattern: "$STR" + - metavariable-regex: + metavariable: "$REPLACE_CHAR" + regex: "(.*\\\\r\\\\n.*)" + - metavariable-regex: + metavariable: "$REPLACE" + regex: "(?!(\\\\r\\\\n))" + - pattern: "org.owasp.encoder.Encode.forUriComponent(...)" + - pattern: "org.owasp.encoder.Encode.forUri(...)" + - pattern: "java.net.URLEncoder.encode(..., $CHARSET)" + pattern-sinks: + - pattern: "($RES: HttpServletResponse).setHeader(\"$KEY\", ...);" + - pattern: "($RES: HttpServletResponse).addHeader(\"$KEY\", ...);" + - pattern: "($WRP: HttpServletResponseWrapper).setHeader(\"$KEY\", ...);" + - pattern: "($WRP: HttpServletResponseWrapper).addHeader(\"$KEY\", ...);" + pattern-sources: + - pattern: "($REQ: HttpServletRequest).getParameter(...);" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/cors/rule-PermissiveCORSInjection.yml b/data/rules/gitlab-sast-rules/scala/cors/rule-PermissiveCORSInjection.yml new file mode 100644 index 0000000..786d257 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/cors/rule-PermissiveCORSInjection.yml @@ -0,0 +1,43 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_cors_rule-PermissiveCORSInjection" + languages: + - "java" + message: | + Prior to HTML5, Web browsers enforced the Same Origin Policy which ensures that in order for + JavaScript to access the contents of a Web page, both the JavaScript and the Web page must + originate from the same domain. Without the Same Origin Policy, a malicious website could serve + up JavaScript that loads sensitive information from other websites using a client's + credentials, cull through it, and communicate it back to the attacker. HTML5 makes it possible + for JavaScript to access data across domains if a new HTTP header called + Access-Control-Allow-Origin is defined. With this header, a Web server defines which other + domains are allowed to access its domain using cross-origin requests. However, caution should + be taken when defining the header because an overly permissive CORS policy will allow a + malicious application to communicate with the victim application in an inappropriate way, + leading to spoofing, data theft, relay and other attacks. + metadata: + category: "security" + cwe: "CWE-942" + owasp: + - "A5:2017-Broken Access Control" + - "A05:2021-Security Misconfiguration" + - "A02:2025-Security Misconfiguration" + shortDescription: "Permissive Cross-domain Policy with Untrusted Domains" + technology: + - "java" + security-severity: "Low" + mode: "taint" + pattern-sinks: + - patterns: + - pattern-either: + - pattern: "(HttpServletResponse $RES).setHeader(\"$HEADER\", ...)" + - pattern: "(HttpServletResponse $RES).addHeader(\"$HEADER\", ...)" + - metavariable-regex: + metavariable: "$HEADER" + regex: "(?i)(Access-Control-Allow-Origin)" + pattern-sources: + - pattern: "(HttpServletRequest $REQ).getParameter(...)" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherDESInsecure.yml b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherDESInsecure.yml new file mode 100644 index 0000000..dce61a4 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherDESInsecure.yml @@ -0,0 +1,29 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_crypto_rule-CipherDESInsecure" + languages: + - "scala" + message: | + DES is considered strong ciphers for modern applications. Currently, NIST recommends the usage + of AES block ciphers instead of DES. + metadata: + category: "security" + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Inadequate Encryption Strength" + technology: + - "scala" + security-severity: "Medium" + patterns: + - pattern-inside: |- + javax.crypto.Cipher.getInstance("$ALG") + - metavariable-regex: + metavariable: "$ALG" + regex: "^(DES)/.*" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherDESedeInsecure.yml b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherDESedeInsecure.yml new file mode 100644 index 0000000..e31dcb8 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherDESedeInsecure.yml @@ -0,0 +1,29 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_crypto_rule-CipherDESedeInsecure" + languages: + - "scala" + message: | + Triple DES (also known as 3DES or DESede) is considered strong ciphers for modern + applications. NIST recommends the usage of AES block ciphers instead of 3DES. + metadata: + category: "security" + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Inadequate Encryption Strength" + technology: + - "scala" + security-severity: "Medium" + patterns: + - pattern-inside: |- + javax.crypto.Cipher.getInstance("$ALG") + - metavariable-regex: + metavariable: "$ALG" + regex: "^(DESede)/.*" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherECBMode.yml b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherECBMode.yml new file mode 100644 index 0000000..f1ccbc8 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherECBMode.yml @@ -0,0 +1,29 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_crypto_rule-CipherECBMode" + languages: + - "scala" + message: | + An authentication cipher mode which provides better confidentiality of the encrypted data + should be used instead of Electronic Code Book (ECB) mode, which does not provide good + confidentiality. Specifically, ECB mode produces the same output for the same input each time. + This allows an attacker to intercept and replay the data. + metadata: + category: "security" + cwe: "CWE-326" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Inadequate Encryption Strength" + technology: + - "scala" + security-severity: "Medium" + patterns: + - pattern-inside: |- + javax.crypto.Cipher.getInstance("...") + - pattern-regex: "(AES|DES(ede)?)(/ECB/*)" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherIntegrity.yml b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherIntegrity.yml new file mode 100644 index 0000000..278c234 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherIntegrity.yml @@ -0,0 +1,35 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_crypto_rule-CipherIntegrity" + languages: + - "scala" + message: | + The ciphertext produced is susceptible to alteration by an adversary. This mean that the + cipher provides no way to detect that the data has been tampered with. If the ciphertext can be + controlled by an attacker, it could be altered without detection. + metadata: + category: "security" + cwe: "CWE-353" + owasp: + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + shortDescription: "Missing Support for Integrity Check" + technology: + - "scala" + security-severity: "Medium" + patterns: + - pattern-inside: |- + javax.crypto.Cipher.getInstance("...") + - pattern-either: + - pattern-regex: "(/CBC/PKCS5Padding)" + - pattern-regex: "(AES|DES(ede)?)(/ECB/*)" + - pattern-regex: "(AES|DES(ede)?)(/CBC/*)" + - pattern-regex: "(AES|DES(ede)?)(/OFB/*)" + - pattern-regex: "(AES|DES(ede)?)(/CTR/*)" + - pattern-not-regex: ".*/(CCM|CWC|OCB|EAX|GCM)/.*" + - pattern-not-regex: "^(RSA)/.*" + - pattern-not-regex: "^(ECIES)$" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherPaddingOracle.yml b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherPaddingOracle.yml new file mode 100644 index 0000000..e6223d5 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/crypto/rule-CipherPaddingOracle.yml @@ -0,0 +1,31 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_crypto_rule-CipherPaddingOracle" + languages: + - "scala" + message: | + This specific mode of CBC with PKCS5Padding is susceptible to padding oracle attacks. An + adversary could potentially decrypt the message if the system exposed the difference between + plaintext with invalid padding or valid padding. The distinction between valid and invalid + padding is usually revealed through distinct error messages being returned for each condition. + metadata: + category: "security" + cwe: "CWE-327" + owasp: + - "A3:2017-Sensitive Data Exposure" + - "A02:2021-Cryptographic Failures" + - "A04:2025-Cryptographic Failures" + shortDescription: "Use of a broken or risky cryptographic algorithm" + technology: + - "scala" + security-severity: "Medium" + patterns: + - pattern-inside: |- + javax.crypto.Cipher.getInstance("...") + - pattern-regex: "(/CBC/PKCS5Padding)" + - pattern-not-regex: "^(RSA)/.*" + - pattern-not-regex: "^(ECIES)$" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/inject/rule-CustomInjectionSQLString.yml b/data/rules/gitlab-sast-rules/scala/inject/rule-CustomInjectionSQLString.yml new file mode 100644 index 0000000..af83591 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/inject/rule-CustomInjectionSQLString.yml @@ -0,0 +1,50 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_inject_rule-CustomInjectionSQLString" + languages: + - "scala" + message: | + The method identified is susceptible to injection. The input should be validated and properly + escaped. + metadata: + category: "security" + cwe: "CWE-89" + owasp: + - "A1:2017-Injection" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper Neutralization of Special Elements used in an SQL + Command ('SQL Injection')" + technology: + - "scala" + security-severity: "High" + patterns: + - pattern-inside: | + def $FOO(..., $SQLIN: String, ...): $TYPE = { + ... + } + - pattern-either: + - pattern: | + "$SQL_STR" + $SQLIN + - pattern: "String.format(\"$SQL_STR\", ... + $SQLIN + ...)" + - pattern: | + "$SQL_STR".concat(...) + - pattern: "(StringBuilder $BUILDER). ... .append(\"$SQL_STR\")" + - patterns: + - pattern-inside: | + StringBuilder $BUILDER = new StringBuilder(... + "$SQL_STR" + ...); + ... + - pattern: "$BUILDER.append(...)" + - pattern-not: "$BUILDER.append(\"...\")" + - patterns: + - pattern-inside: | + $QUERY = "$SQL_STR"; + ... + - pattern: "$QUERY += ..." + - metavariable-regex: + metavariable: "$SQL_STR" + regex: "(?i)(select|insert|create|update|alter|delete|drop)\\b" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/scala/inject/rule-PathTraversalIn.yml b/data/rules/gitlab-sast-rules/scala/inject/rule-PathTraversalIn.yml new file mode 100644 index 0000000..ac69982 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/inject/rule-PathTraversalIn.yml @@ -0,0 +1,65 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_inject_rule-PathTraversalIn" + languages: + - "scala" + message: | + A file is opened to read its content. The filename comes from an input parameter. If an + unfiltered parameter is passed to this file API, files from an arbitrary filesystem location + could be read. This rule identifies potential path traversal vulnerabilities. In many cases, + the constructed file path cannot be controlled by the user. + metadata: + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + category: "security" + cwe: "CWE-22" + shortDescription: "Improper limitation of a pathname to a restricted directory + ('Path Traversal')" + technology: + - "scala" + security-severity: "Medium" + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + def $FUNC(...,$ARGS: Array[String], ...): $TYPE = { + ... + } + - pattern-inside: | + $VAR = $ARGS($IDX) + ... + - pattern-inside: | + def $FUNC(...,$VAR: String, ...): $TYPE = { + ... + } + - pattern-not-inside: | + ... + org.apache.commons.io.FilenameUtils.getName($VAR) + ... + - pattern-either: + - patterns: + - pattern-inside: | + $U = new java.net.URI($VAR) + ... + - pattern-either: + - pattern: "new java.io.File($U)" + - pattern: "java.nio.file.Paths.get($U)" + - pattern: "new java.io.RandomAccessFile(..., $VAR,...)" + - pattern: "new java.io.FileReader(<...$VAR...>, ...)" + - pattern: "new javax.activation.FileDataSource(..., $VAR, ...)" + - pattern: "new java.io.FileInputStream(..., $VAR, ...)" + - pattern: "new java.io.File(<...$VAR...>, ...)" + - pattern: "java.nio.file.Paths.get(...,$VAR,...)" + - pattern: "java.io.File.createTempFile(...,$VAR, ...)" + - pattern: "java.io.File.createTempDirectory(...,$VAR,...)" + - pattern: "java.nio.file.Files.createTempFile(..., $VAR, ...)" + - pattern: "java.nio.file.Files.createTempDirectory(..., $VAR, ...)" + - pattern: "scala.io.Source.from(<...$VAR...>)" + - pattern: "scala.io.Source.fromFile(<...$VAR...>)" + - pattern: "scala.io.Source.fromString(<...$VAR...>)" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/inject/rule-PathTraversalOut.yml b/data/rules/gitlab-sast-rules/scala/inject/rule-PathTraversalOut.yml new file mode 100644 index 0000000..05f4946 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/inject/rule-PathTraversalOut.yml @@ -0,0 +1,51 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_inject_rule-PathTraversalOut" + languages: + - "scala" + message: | + A file is opened to write to its contents. The filename comes from an input parameter. If an + unfiltered parameter is passed to this file API, files at an arbitrary filesystem location + could be modified. This rule identifies potential path traversal vulnerabilities. In many + cases, the constructed file path cannot be controlled by the user. + metadata: + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + category: "security" + cwe: "CWE-22" + shortDescription: "Improper limitation of a pathname to a restricted directory + ('Path Traversal')" + technology: + - "scala" + security-severity: "High" + mode: "taint" + pattern-sanitizers: + - pattern: "org.apache.commons.io.FilenameUtils.getName(...)" + pattern-sinks: + - patterns: + - pattern-inside: |- + new java.io.FileWriter($PATH, ...) + - pattern: "$PATH" + - patterns: + - pattern-inside: |- + new java.io.FileOutputStream($PATH, ...) + - pattern: "$PATH" + pattern-sources: + - patterns: + - pattern-inside: | + def $FUNC(..., $ARGS: Array[String], ...): $TYPE = { + ... + } + - pattern: "$ARGS[$IDX]" + - patterns: + - pattern-inside: | + def $FUNC(..., $VAR: String, ...): $TYPE = { + ... + } + - pattern: "$VAR" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/scala/inject/rule-SpotbugsPathTraversalAbsolute.yml b/data/rules/gitlab-sast-rules/scala/inject/rule-SpotbugsPathTraversalAbsolute.yml new file mode 100644 index 0000000..656a288 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/inject/rule-SpotbugsPathTraversalAbsolute.yml @@ -0,0 +1,62 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_inject_rule-SpotbugsPathTraversalAbsolute" + languages: + - "scala" + message: | + "The software uses an HTTP request parameter to construct a pathname that should be within a + restricted directory, but it does not properly neutralize absolute path sequences such as + "/abs/path" that can resolve to a location that is outside of that directory. See + http://cwe.mitre.org/data/definitions/36.html for more information." + metadata: + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + category: "security" + cwe: "CWE-22" + shortDescription: "Improper limitation of a pathname to a restricted directory + ('Path Traversal')" + technology: + - "scala" + security-severity: "Info" + mode: "taint" + pattern-sanitizers: + - pattern: "org.apache.commons.io.FilenameUtils.getName(...)" + pattern-sinks: + - patterns: + - pattern-inside: | + $U = new java.net.URI($VAR) + - pattern-either: + - pattern-inside: |- + new java.io.File($U) + - pattern-inside: |- + java.nio.file.Paths.get($U) + - pattern: "$VAR" + - patterns: + - pattern-inside: |- + new java.io.RandomAccessFile($INPUT,...) + - pattern: "$INPUT" + - pattern: "new java.io.FileReader(...)" + - pattern: "new javax.activation.FileDataSource(...)" + - pattern: "new java.io.FileInputStream(...)" + - pattern: "new java.io.File(...)" + - pattern: "java.nio.file.Paths.get(...)" + - pattern: "java.io.File.createTempFile(...)" + - pattern: "java.io.File.createTempDirectory(...)" + - pattern: "java.nio.file.Files.createTempFile(...)" + - pattern: "java.nio.file.Files.createTempDirectory(...)" + - patterns: + - pattern-inside: |- + new java.io.FileWriter($PATH, ...) + - pattern: "$PATH" + - patterns: + - pattern-inside: |- + new java.io.FileOutputStream($PATH, ...) + - pattern: "$PATH" + pattern-sources: + - pattern: "($REQ: HttpServletRequest ).getParameter(...)" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/scala/inject/rule-SpotbugsPathTraversalRelative.yml b/data/rules/gitlab-sast-rules/scala/inject/rule-SpotbugsPathTraversalRelative.yml new file mode 100644 index 0000000..d3715cc --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/inject/rule-SpotbugsPathTraversalRelative.yml @@ -0,0 +1,68 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_inject_rule-SpotbugsPathTraversalRelative" + languages: + - "scala" + message: | + "The software uses an HTTP request parameter to construct a pathname that should be within a + restricted directory, but it does not properly neutralize sequences such as ".." that can + resolve to a location that is outside of that directory. See + http://cwe.mitre.org/data/definitions/23.html for more information." + metadata: + owasp: + - "A5:2017-Broken Access Control" + - "A01:2021-Broken Access Control" + - "A01:2025-Broken Access Control" + category: "security" + cwe: "CWE-22" + shortDescription: "Improper limitation of a pathname to a restricted directory + ('Path Traversal')" + technology: + - "scala" + security-severity: "Info" + mode: "taint" + pattern-sanitizers: + - pattern: "org.apache.commons.io.FilenameUtils.getName(...)" + pattern-sinks: + - patterns: + - pattern-inside: | + $U = new java.net.URI($VAR) + - pattern-either: + - pattern-inside: |- + new java.io.File($U) + - pattern-inside: |- + java.nio.file.Paths.get($U) + - pattern: "$VAR" + - patterns: + - pattern-inside: |- + new java.io.RandomAccessFile($INPUT,...) + - pattern: "$INPUT" + - pattern: "new java.io.FileReader(...)" + - pattern: "new javax.activation.FileDataSource(...)" + - pattern: "new java.io.FileInputStream(...)" + - pattern: "new java.io.File(...)" + - pattern: "java.nio.file.Paths.get(...)" + - pattern: "java.io.File.createTempFile(...)" + - pattern: "java.io.File.createTempDirectory(...)" + - pattern: "java.nio.file.Files.createTempFile(...)" + - pattern: "java.nio.file.Files.createTempDirectory(...)" + - patterns: + - pattern-inside: |- + new java.io.FileWriter($PATH, ...) + - pattern: "$PATH" + - patterns: + - pattern-inside: |- + new java.io.FileOutputStream($PATH, ...) + - pattern: "$PATH" + pattern-sources: + - patterns: + - pattern-inside: | + $P = ($REQ: HttpServletRequest ).getParameter(...); + ... + - pattern-either: + - pattern: "$P + ..." + - pattern: "... + $P" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/scala/ldap/rule-EntryPoisoning.yml b/data/rules/gitlab-sast-rules/scala/ldap/rule-EntryPoisoning.yml new file mode 100644 index 0000000..7d7b6d9 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/ldap/rule-EntryPoisoning.yml @@ -0,0 +1,25 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_ldap_rule-EntryPoisoning" + languages: + - "scala" + message: | + Without proper access control, executing an LDAP statement that contains a + user-controlled value can allow an attacker to abuse poorly configured LDAP + context + metadata: + category: "security" + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + shortDescription: "Deserialization of untrusted data" + security-severity: "High" + patterns: + - pattern: "new javax.naming.directory.SearchControls($SCOPE, $CLIMIT, $TLIMIT, + $ATTR, true, $DEREF)" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/password/rule-ConstantDBPassword.yml b/data/rules/gitlab-sast-rules/scala/password/rule-ConstantDBPassword.yml new file mode 100644 index 0000000..222d5b8 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/password/rule-ConstantDBPassword.yml @@ -0,0 +1,41 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_password_rule-ConstantDBPassword" + patterns: + - pattern: "java.sql.DriverManager.getConnection($URI, $USR, \"...\");" + - pattern-not: "java.sql.DriverManager.getConnection($URI, $USR, \"\");" + message: | + A potential hard-coded password was identified in a database connection string. + Passwords should not be stored directly in code + but loaded from secure locations such as a Key Management System (KMS). + + The purpose of using a Key Management System is so access can be audited and keys easily + rotated + in the event of a breach. By hardcoding passwords, it will be extremely difficult to determine + when or if, a key is compromised. + + The recommendation on which KMS to use depends on the environment the application is running + in: + + - For Google Cloud Platform consider [Cloud Key Management](https://cloud.google.com/kms/docs) + - For Amazon Web Services consider [AWS Key Management](https://aws.amazon.com/kms/) + - For on premise or other alternatives to cloud providers, consider [Hashicorp's + Vault](https://www.vaultproject.io/) + - For other cloud providers, please see their documentation + metadata: + category: "security" + cwe: "CWE-259" + owasp: + - "A2:2017-Broken Authentication" + - "A07:2021-Identification and Authentication Failures" + - "A07:2025-Authentication Failures" + shortDescription: "Use of Hard-coded Password" + technology: + - "scala" + security-severity: "Critical" + severity: "ERROR" + languages: + - "scala" diff --git a/data/rules/gitlab-sast-rules/scala/password/rule-EmptyDBPassword.yml b/data/rules/gitlab-sast-rules/scala/password/rule-EmptyDBPassword.yml new file mode 100644 index 0000000..49fff01 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/password/rule-EmptyDBPassword.yml @@ -0,0 +1,45 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_password_rule-EmptyDBPassword" + patterns: + - pattern: "java.sql.DriverManager.getConnection($URI, $USR, \"\");" + message: | + The application does not provide authentication when communicating a database + server. It is strongly recommended that the database server be configured with + authentication and restrict what queries users can execute. + + Please see your database server's documentation on how to configure a password. + + Additionally, passwords should not be stored directly in code + but loaded from secure locations such as a Key Management System (KMS). + + The purpose of using a Key Management System is so access can be audited and keys easily + rotated + in the event of a breach. By hardcoding passwords, it will be extremely difficult to determine + when or if, a key is compromised. + + The recommendation on which KMS to use depends on the environment the application is running + in: + + - For Google Cloud Platform consider [Cloud Key Management](https://cloud.google.com/kms/docs) + - For Amazon Web Services consider [AWS Key Management](https://aws.amazon.com/kms/) + - For on premise or other alternatives to cloud providers, consider [Hashicorp's + Vault](https://www.vaultproject.io/) + - For other cloud providers, please see their documentation + metadata: + category: "security" + cwe: "CWE-259" + owasp: + - "A2:2017-Broken Authentication" + - "A07:2021-Identification and Authentication Failures" + - "A07:2025-Authentication Failures" + shortDescription: "Use of Hard-coded Password" + technology: + - "scala" + security-severity: "Critical" + languages: + - "scala" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/password/rule-HardcodePassword.yml b/data/rules/gitlab-sast-rules/scala/password/rule-HardcodePassword.yml new file mode 100644 index 0000000..3487865 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/password/rule-HardcodePassword.yml @@ -0,0 +1,51 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_password_rule-HardcodePassword" + languages: + - "scala" + message: | + A potential hard-coded password was identified in the source code. + Passwords should not be stored directly in code + but loaded from secure locations such as a Key Management System (KMS). + + The purpose of using a Key Management System is so access can be audited and keys easily + rotated + in the event of a breach. By hardcoding passwords, it will be extremely difficult to determine + when or if, a key is compromised. + + The recommendation on which KMS to use depends on the environment the application is running + in: + + - For Google Cloud Platform consider [Cloud Key Management](https://cloud.google.com/kms/docs) + - For Amazon Web Services consider [AWS Key Management](https://aws.amazon.com/kms/) + - For on premise or other alternatives to cloud providers, consider [Hashicorp's + Vault](https://www.vaultproject.io/) + - For other cloud providers, please see their documentation + metadata: + category: "security" + cwe: "CWE-259" + owasp: + - "A2:2017-Broken Authentication" + - "A07:2021-Identification and Authentication Failures" + - "A07:2025-Authentication Failures" + shortDescription: "Use of Hard-coded Password" + technology: + - "scala" + security-severity: "High" + pattern-either: + - pattern: "java.security.KeyStore.PasswordProtection(\"...\".toCharArray())" + - pattern: "java.security.KeyStore.getInstance(...).load(..., \"...\".toCharArray())" + - pattern: "($KS: java.security.KeyStore).load(..., \"...\".toCharArray())" + - pattern: "KeyManagerFactory.getInstance(...).init(..., \"...\".toCharArray())" + - pattern: "($KMF: KeyManagerFactory).init(..., \"...\".toCharArray())" + - pattern: "PBEKeySpec(\"...\", ...)" + - pattern: "PasswordAuthentication(\"...\", \"...\")" + - pattern: "($CB: PasswordCallback).setPassword(\"...\")" + - pattern: "KerberosKey(...,\"...\",...)" + - pattern: "java.sql.DriverManager.getConnection(..., \"...\")" + - pattern: "io.vertx.ext.web.handler.CSRFHandler.create(..., \"...\")" + - pattern: "$S.setPassword(\"...\")" + severity: "ERROR" diff --git a/data/rules/gitlab-sast-rules/scala/xml/rule-XmlDecoder.yml b/data/rules/gitlab-sast-rules/scala/xml/rule-XmlDecoder.yml new file mode 100644 index 0000000..334db23 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/xml/rule-XmlDecoder.yml @@ -0,0 +1,28 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_xml_rule-XmlDecoder" + languages: + - "scala" + message: | + Avoid using XMLDecoder to parse content from an untrusted source. + metadata: + category: "security" + cwe: "CWE-502" + owasp: + - "A8:2017-Insecure Deserialization" + - "A08:2021-Software and Data Integrity Failures" + - "A08:2025-Software or Data Integrity Failures" + shortDescription: "Deserialization of Untrusted Data" + security-severity: "High" + patterns: + - pattern-inside: | + $D = new java.beans.XMLDecoder($IN); + ... + - pattern-not-inside: | + $DX = new java.beans.XMLDecoder("..."); + ... + - pattern: "$D.readObject" + severity: "WARNING" diff --git a/data/rules/gitlab-sast-rules/scala/xss/rule-XSSServlet.yml b/data/rules/gitlab-sast-rules/scala/xss/rule-XSSServlet.yml new file mode 100644 index 0000000..db96937 --- /dev/null +++ b/data/rules/gitlab-sast-rules/scala/xss/rule-XSSServlet.yml @@ -0,0 +1,47 @@ +# yamllint disable +# License: MIT (c) GitLab Inc. +# yamllint enable +--- +rules: +- id: "scala_xss_rule-XSSServlet" + languages: + - "scala" + message: | + A potential XSS was found. It could be used to execute unwanted JavaScript in a + client's browser. + metadata: + category: "security" + cwe: "CWE-79" + owasp: + - "A7:2017-Cross-Site Scripting (XSS)" + - "A03:2021-Injection" + - "A05:2025-Injection" + shortDescription: "Improper Neutralization of Input During Web Page Generation + ('Cross-site Scripting')" + security-severity: "Info" + mode: "taint" + pattern-sanitizers: + - patterns: + - pattern-inside: |- + org.owasp.encoder.Encode.forHtml($TAINTED); + - pattern: "$TAINTED" + pattern-sinks: + - patterns: + - pattern-inside: |- + def $FUNC(..., $RES: HttpServletResponse, ...): $TYPE = {...} + - pattern-inside: | + $WRITER = $RES.getWriter; + ... + - pattern: "$WRITER.write($DATA,...);" + - pattern: "$DATA" + - patterns: + - pattern-inside: |- + def $FUNC(..., $RES: HttpServletResponse, ...): $TYPE = {...} + - pattern: "$RES.getWriter.write($DATA,...);" + - pattern: "$DATA" + pattern-sources: + - patterns: + - pattern-inside: |- + def $FUNC(..., $REQ: HttpServletRequest, ...): $TYPE = {...} + - pattern: "$REQ.getParameter(...);" + severity: "WARNING" diff --git a/data/rules/selection.json b/data/rules/selection.json new file mode 100644 index 0000000..36716f8 --- /dev/null +++ b/data/rules/selection.json @@ -0,0 +1,169 @@ +{ + "version": 1, + "about": "Lane B's recall selection: which rule corpora, directories, rule files and native-analyser rules Lane B runs, and which paths it never reports from. Decided by the owner on 2026-10-03 from the per-rule counts in eval/results/candidates-per-scan.json. data/rules/MANIFEST.json is generated from this file by `python -m anvil_eval.vendor_rules` and internal/recall verifies both.", + "decided_by": "the owner", + "decided_on": "2026-10-03", + "tools": [ + { + "name": "opengrep", + "version": "1.26.0", + "repository": "https://github.com/opengrep/opengrep", + "licence": "LGPL-2.1", + "licence_evidence": "eval/tools/opengrep/LICENSES.md section 1, the LICENSE body at tag v1.26.0", + "role": "runs the vendored rule corpora; subprocess only" + }, + { + "name": "gosec", + "version": "2.29.0", + "repository": "https://github.com/securego/gosec", + "licence": "Apache-2.0", + "licence_evidence": "LICENSE.txt in gosec_2.29.0_linux_amd64.tar.gz (sha256 6431b119741c1f4a50fdfcf94e782e16b9e642afc8c7fa9b5d39d48bf3003095), read 2026-10-03", + "role": "Go, with its own rules" + }, + { + "name": "bandit", + "version": "1.9.4", + "repository": "https://github.com/PyCQA/bandit", + "licence": "Apache-2.0", + "licence_evidence": "the LICENSE body at tag 1.9.4, read 2026-10-03", + "role": "Python, with its own rules" + } + ], + "corpora": [ + { + "name": "gitlab-sast-rules", + "repository": "https://gitlab.com/gitlab-org/security-products/sast-rules", + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "licence_file": "LICENSE", + "rule_suffix": ".yml", + "directories": [ + "csharp", + "go", + "java", + "javascript", + "python", + "scala" + ] + }, + { + "name": "0xdea-semgrep-rules", + "repository": "https://github.com/0xdea/semgrep-rules", + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "licence_file": "LICENSE", + "rule_suffix": ".yaml", + "directories": [ + "rules/c" + ] + } + ], + "excluded_directories": [ + { + "corpus": "gitlab-sast-rules", + "directory": "c", + "reason": "Every one of its 62 rule files is headed 'License: GPL 2.0': they are generated from flawfinder, which is GPL-2.0. The repository LICENSE keeps third-party components under their own licences. Excluded by the owner on 2026-10-03; C coverage comes from 0xdea's MIT rules." + }, + { + "corpus": "0xdea-semgrep-rules", + "directory": "rules/noisy", + "reason": "Its one rule, bad-words, flags comments (CWE-546) and its author lists it as 'potentially too noisy and unsuitable for general use'. It was 5,249 of curl's 10,714 candidates." + } + ], + "excluded_rule_files": [ + { + "corpus": "gitlab-sast-rules", + "path": "python/assert/rule-assert-used.yml", + "reason": "Assert statements (CWE-754), the same informational finding as bandit B101." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/interesting-api-calls.yaml", + "reason": "A code-review heuristic: lists calls worth a look (CWE-676), not a weakness. 1,440 of curl's candidates outside its tests." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/typos.yaml", + "reason": "A code-review heuristic for operator typos (CWE-480). 977 of curl's candidates outside its tests." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/signed-unsigned-conversion.yaml", + "reason": "A code-review heuristic flagging every signed-to-unsigned conversion (CWE-195). 493 of curl's candidates outside its tests." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/suspicious-assert.yaml", + "reason": "A code-review heuristic for assert usage (CWE-754)." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/missing-default-in-switch.yaml", + "reason": "A code-quality pattern (CWE-478), not a weakness." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/pointer-subtraction.yaml", + "reason": "A code-review heuristic (CWE-469) flagging every pointer subtraction." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/missing-break-in-switch.yaml", + "reason": "A code-quality pattern (CWE-484), not a weakness." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/missing-return.yaml", + "reason": "A code-quality pattern (CWE-393), not a weakness." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/argv-envp-access.yaml", + "reason": "A code-review heuristic marking every argv and envp access (CWE-15)." + }, + { + "corpus": "0xdea-semgrep-rules", + "path": "rules/c/high-entropy-assignment.yaml", + "reason": "A code-review heuristic for high-entropy string literals (CWE-540)." + }, + { + "corpus": "gitlab-sast-rules", + "path": "java/endpoint/rule-UnvalidatedRedirect.yml", + "reason": "Its companion test file states no licence, and GitLab's mappings tie it to a find-sec-bugs rule id (an LGPL-3.0 analyser), so nothing but its own MIT header speaks for it. Dropped by the owner on 2026-10-04, after the same-family review." + } + ], + "exclude_by_derived_licence": { + "licences": [ + "LGPL-3.0" + ], + "decided": "the owner, 2026-10-03, after the same-family review (docs/reviews/lane-b-gate.md)", + "reason": "GitLab ported most of its rules from other analysers and labels each rule's companion test file with the upstream licence. 131 rules headed 'MIT (c) GitLab Inc.' have companions labelled 'LGPL-3.0 License (c) find-sec-bugs' (Java 42, Scala 68) or '(c) security-code-scan' (C# 21). A rule's licence is the stricter of the two headers, and rules derived under LGPL-3.0 are left out; the manifest lists each one." + }, + "excluded_tool_rules": [ + { + "tool": "bandit", + "rule": "B101", + "reason": "Assert statements (CWE-703): informational. 1,054 of flask's 1,090 candidates." + } + ], + "excluded_paths": { + "reason": "Lane B reports on shipped first-party code. Test, test-data, documentation and example trees are not shipped; they were 1,071 of flask's 1,090 candidates and 2,131 of curl's 10,714.", + "directories": [ + "test", + "tests", + "testing", + "testdata", + "__tests__", + "spec", + "fixtures", + "doc", + "docs", + "example", + "examples" + ], + "file_globs": [ + "*_test.go", + "test_*.py", + "*_test.py", + "conftest.py" + ] + } +} diff --git a/docs/controls.md b/docs/controls.md index fc25ba2..bc78dbe 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -2239,3 +2239,24 @@ U1c is the one to read first if you are wiring DAST into a scan path. tree calls `Provision`, `SetupNetns` or `AssertContainment`, and the two halves of the containment story therefore do not compose yet. That is the dynamic tier exit gate's, not target provisioning's or network containment's — it is recorded here so it cannot be forgotten. + +--- + +# LANE B (Phase 6, 2026-10-03) + +No `t.Skip` was added. The one Lane B control that needs installed tools is written the way the Trivy one is: + +- **`TestTheFixtureWithTheRealTools`** (`internal/recall`) runs opengrep 1.26.0, gosec 2.29.0 and bandit 1.9.4 over + `testdata/laneb-fixture` and asserts the exact twelve candidates. Where a tool is absent it does not skip: it + asserts that `Prepare` refused with a missing-tool error and logs it. With `ANVIL_LANEB_E2E=1` an absent tool + fails the test. CI's "Lane B end to end (real tools)" job installs the pinned tools, sets the opt-in, runs + `test/e2e/laneb.sh`, and proves the opt-in is fatal by hiding opengrep and requiring a failure. +- **The Go job proves the chain without the tools** by replaying their real reports + (`internal/recall/recalltest`, recorded from the fixture on 2026-10-03 with `ANVIL_LANEB_RECORD=1`). A replay is + only as good as the recording, and the real-tools test, which asserts the same twelve candidates, is what holds + it to the tools. + +Measured on this machine on 2026-10-03: with the tools present the real-tools test passes; mutating +`--disable-nosem` or `--ignore-nosec` out of the argument vectors, or disabling the GitLab directory allowlist, +fails it or `TestThePackGuardsFire`, and mapping a candidate to `true_positive` fails +`TestEveryCandidateReachesTheRecordUnconfirmed`. diff --git a/docs/gates/lane-b.json b/docs/gates/lane-b.json new file mode 100644 index 0000000..588f6b5 --- /dev/null +++ b/docs/gates/lane-b.json @@ -0,0 +1,239 @@ +{ + "gate": "Lane B exit gate (plan node lanebgate), and Phase 6's exit gate (plan node p6)", + "written_on": "2026-10-03", + "written_by": "the Phase 6 agent; every PROVEN row names what showed it, and nothing passes by assertion", + "rows": [ + { + "criterion": "On the fixture repositories, Lane B findings carry rule provenance", + "status": "PROVEN", + "evidence": [ + { + "test": "TestEveryCandidateReachesTheRecordUnconfirmed", + "path": "internal/scan/laneb_test.go" + }, + { + "test": "TestTheFixtureWithTheRealTools", + "path": "internal/recall/recall_test.go" + }, + { + "test": "TestARuleReferenceMustResolveToCompleteProvenance", + "path": "internal/record/contract_test.go" + }, + { + "command": "sh test/e2e/laneb.sh", + "path": "test/e2e/laneb.sh" + } + ] + }, + { + "criterion": "Lane B findings reach the record as unconfirmed, and the verdict mapping onto the record is tested", + "status": "PROVEN", + "evidence": [ + { + "test": "TestEveryCandidateReachesTheRecordUnconfirmed", + "path": "internal/scan/laneb_test.go", + "note": "seen failing on 2026-10-03 with VerdictFor mutated to true_positive" + }, + { + "test": "TestVerdictForIsUnconfirmedWhateverTheCandidate", + "path": "internal/laneb/laneb_test.go" + } + ] + }, + { + "criterion": "Candidates per scan are under 500 a full scan and 50 a push, or the recall tier is re-scoped", + "status": "PROVEN", + "evidence": [ + { + "artifact": "eval/results/candidates-per-scan.json", + "path": "eval/results/candidates-per-scan.json", + "note": "re-measured after the review's fixes and the licence cut; see the artifact" + }, + { + "artifact": "data/rules/selection.json", + "path": "data/rules/selection.json" + } + ], + "owner": "the register row's decision still reads FAIL, set on the old number; changing it is the owner's" + }, + { + "criterion": "The ranker ships only on a measured win over chance", + "status": "PROVEN (no ranker ships)", + "evidence": [ + { + "artifact": "eval/results/code-metrics-baseline.json", + "path": "eval/results/code-metrics-baseline.json", + "note": "on PrimeVul's pairs, precision at recall 0.70 was 0.498 for the model and 0.492 for the regression, both at chance (0.5)" + }, + { + "test": "TestEveryCandidateReachesTheRecordUnconfirmed", + "path": "internal/scan/laneb_test.go", + "note": "asserts result.rank is unset on every Lane B result" + } + ], + "decision": "No ranker for v1, chosen by the owner on 2026-10-03: no labelled sample of Anvil's own candidates exists, so no win over chance can be measured on them, and the only measurement there is shows neither ranker beating chance. The encoder node is not built." + }, + { + "criterion": "Engines are subprocess only", + "status": "PROVEN", + "evidence": [ + { + "test": "TestTheRecallTierLinksNoTool", + "path": "internal/recall/subprocess_test.go", + "note": "seen failing on 2026-10-03 with a third-party import planted" + }, + { + "test": "TestTheArgumentVectorsSwitchOffTargetSuppression", + "path": "internal/recall/recall_test.go" + } + ] + }, + { + "criterion": "Corpora are vendored at pinned commits, licence-verified from their bodies, with GitLab's directory allowlist enforced by a test with a negative control", + "status": "PROVEN", + "evidence": [ + { + "test": "TestTheVendoredPackVerifies", + "path": "internal/recall/recall_test.go" + }, + { + "test": "TestThePackGuardsFire", + "path": "internal/recall/recall_test.go", + "note": "ten mutations, each refused; disabling the allowlist check fails its two directory cases (seen 2026-10-03)" + }, + { + "test": "test_a_gpl_rule_refuses_the_whole_run", + "path": "eval/tests/test_vendor_rules.py" + }, + { + "test": "test_an_lgpl_companion_is_left_out_when_the_selection_excludes_it", + "path": "eval/tests/test_vendor_rules.py", + "note": "after the same-family review: a GitLab rule's licence is the stricter of its own header and its companion's; 131 LGPL-3.0-derived rules are left out, by the owner's decision of 2026-10-03" + }, + { + "test": "test_a_companion_decides_when_it_names_an_upstream", + "path": "eval/tests/test_vendor_rules.py" + } + ] + }, + { + "criterion": "100% of candidates carry non-empty provenance (repository, commit or release, licence)", + "status": "PROVEN", + "evidence": [ + { + "test": "TestTheFixtureFromRecordedReports", + "path": "internal/recall/recall_test.go" + }, + { + "test": "TestEmitRefusesIncompleteProvenance", + "path": "internal/laneb/laneb_test.go" + }, + { + "command": "sh test/e2e/laneb.sh", + "path": "test/e2e/laneb.sh" + } + ] + }, + { + "criterion": "Every runner distinguishes tool absent, tool failed and no matches; a missing tool is never a clean scan", + "status": "PROVEN", + "evidence": [ + { + "test": "TestAbsentFailedAndNoMatchesAreThreeAnswers", + "path": "internal/recall/recall_test.go" + }, + { + "test": "TestAMissingRulePackIsAMissingTool", + "path": "internal/scan/laneb_test.go" + }, + { + "test": "TestALaneBScanOfNothingIsNotClean", + "path": "internal/scan/laneb_test.go" + }, + { + "test": "TestTheReviewersEvasionsAreRecorded", + "path": "internal/recall/recall_test.go" + } + ] + }, + { + "criterion": "The scanned repository cannot switch a rule off", + "status": "PROVEN for nosemgrep, #nosec, # nosec, .semgrepignore, .bandit, bandit's and opengrep's default excludes; anything a tool was given and did not analyse is recorded as incomplete. NOT closed: the selection's own path exclusions match by name, kept by the owner as final for v1 (2026-10-04) with the limit stated", + "evidence": [ + { + "test": "TestTheFixtureWithTheRealTools", + "path": "internal/recall/recall_test.go", + "note": "src/suppressed.py is still reported and .semgrepignore and .bandit have no effect; removing --disable-nosem or --ignore-nosec fails it (seen 2026-10-03)" + }, + { + "test": "TestTheReviewersEvasionsAreRecorded", + "path": "internal/recall/recall_test.go", + "note": "another platform's build tags, cgo, Go outside any module, a symbolic link, bandit's default excludes; seen failing without the bandit and symlink fixes" + }, + { + "document": "the exclusions' limit, as the owner kept it", + "path": "internal/record/CONTRACT.md" + } + ] + }, + { + "criterion": "Candidates per scan is exported on every scan", + "status": "PROVEN", + "evidence": [ + { + "test": "TestRecallCandidatesKeepsNotRunApartFromNone", + "path": "internal/store/recall_candidates_test.go" + }, + { + "test": "TestEveryCandidateReachesTheRecordUnconfirmed", + "path": "internal/scan/laneb_test.go", + "note": "asserts scan_run.recall_candidates = 12" + } + ] + }, + { + "criterion": "A checked-in API specification is handed to the dynamic tier's inventory", + "status": "PROVEN on the SAST run; the dynamic tier reading it live is Phase 8's", + "evidence": [ + { + "test": "TestHarvest", + "path": "internal/laneb/laneb_test.go" + }, + { + "command": "sh test/e2e/laneb.sh", + "path": "test/e2e/laneb.sh" + } + ] + }, + { + "criterion": "Per-language coverage is stated in the README", + "status": "PROVEN", + "evidence": [ + { + "document": "README.md, What Lane B covers", + "path": "README.md" + } + ] + }, + { + "criterion": "The LGPL-2.1 obligation of shipping the opengrep binary is stated where packaging is decided", + "status": "PROVEN: stated, and decided by the owner on 2026-10-04 (an operator-installed prerequisite; no Anvil artifact conveys the binary)", + "evidence": [ + { + "document": "the three options and their duties", + "path": "data/LICENSES/opengrep-binary-distribution.md" + } + ] + }, + { + "criterion": "The adversarial review of licence and security conclusions is recorded as same-family", + "status": "PROVEN", + "evidence": [ + { + "document": "the review and what was done about each finding", + "path": "docs/reviews/lane-b-gate.md" + } + ] + } + ] +} diff --git a/docs/reviews/lane-b-gate.md b/docs/reviews/lane-b-gate.md new file mode 100644 index 0000000..4861004 --- /dev/null +++ b/docs/reviews/lane-b-gate.md @@ -0,0 +1,48 @@ +# Lane B exit gate: adversarial review of the licence and security conclusions + +**A same-family review.** The critic was a Claude model reviewing work a Claude model wrote, on 2026-10-03, +read-only, with no network. It shares the author's blind spots, it is not legal advice, and it is not an +independent check: the owner withdrew cross-family critics so that no plan or code goes to a third-party model +provider. Where the critic relied on its own memory rather than a fetched source, that is said below. + +It was asked to break ten conclusions: five on licences (what is vendored and under what licence, the gosec-derived +rules' Apache-2.0 duties, 0xdea, the LGPL analysis of opengrep, what is excluded) and five on security (whether the +scanned repository can suppress Lane B, whether gosec's package loading can be turned against the host, trust +labels, path handling, and whether a missing or failing tool can ever produce a clean scan). + +## Findings, and what was done + +| # | Severity | Finding | Disposition | +|---|---|---|---| +| B1 | blocking | A GitLab rule's "License: MIT (c) GitLab Inc." header is not a reliable licence reading. GitLab labels each rule's companion test file with the analyser it was ported from: 131 vendored rules (Java 42, Scala 68, C# 21) have companions labelled "LGPL-3.0 License (c) find-sec-bugs" or "(c) security-code-scan"; 52 Python rules "Apache 2.0 (c) PyCQA" (bandit); 6 JavaScript rules "MIT (c) JS Foundation". GitLab's `mappings/` agree on the origins. Two Python rules quote bandit's docstrings word for word (verified by script); several Scala messages match find-sec-bugs prose (from the critic's memory, not byte-verified). | **Fixed, by the owner's decision (2026-10-03).** A GitLab rule's licence is now the stricter of its own header and its companion's (`anvil_eval.vendor_rules`, `derive`); `selection.json`'s `exclude_by_derived_licence` leaves out LGPL-3.0-derived rules, and `MANIFEST.json` lists all 131 with their evidence. The 52 bandit-derived rules are Apache-2.0 under bandit's archived licence body; the 6 JavaScript rules are MIT with the JS Foundation's notice recorded. The pack went from 308 rule files to 177, and to 176 when the owner also dropped the one Java rule whose companion states no licence (2026-10-04). Held by `test_an_lgpl_companion_is_left_out_when_the_selection_excludes_it`, `test_a_companion_decides_when_it_names_an_upstream` and `TestTheVendoredPackVerifies`. | +| B2 | blocking | The path exclusions match by directory name, so a repository can put code under `spec/`, `docs/` or `fixtures/` and Lane B will not report it; moving code there turns its findings into "fixed" (verified with a full scan). | **Kept, and the limit stated, by the owner's decision (2026-10-03).** Without the exclusions curl measured 525 candidates, over the budget of 500. Every scan reports how many source files the exclusions kept out; the README and the record contract state that a repository can place code under an excluded name, and the Phase 7 handoff tells the triage gate to treat a finding that is "fixed" with no code change as suspect. | +| S1 | should-fix | gosec's coverage was never checked file by file: Go files in no module, files with another platform's build tags and files under `vendor/` were silently left to opengrep. | **Fixed.** Lane B counts the non-test Go files the Go tool loads per module and compares gosec's `Stats.files`; a shortfall, and Go files in no module, are incomplete coverage; vendored Go files are a note. `TestTheReviewersEvasionsAreRecorded`. | +| S2 | should-fix | Loading packages for gosec ran the C compiler on the repository's cgo code with the operator's environment; a `#include` of a host file put its first line into the scan's problems. | **Fixed.** `CGO_ENABLED=0` (and `GOWORK=off`, with the operator's `GOOS`/`GOARCH` dropped); cgo files are then uncounted by gosec and show as incomplete coverage. The test asserts no host file content reaches the output. | +| S3 | should-fix | bandit applies its default excludes as substrings even to explicitly named files: `.github/…`, `CVSS-tool/…` were skipped. | **Fixed.** `-x` with a sentinel that matches nothing (`BanditNoExclude`); seen failing without it. | +| S4 | should-fix | A symbolic-link `.go` file in a module failed every Lane B run (gosec follows it; Lane B's snippet read refuses it). | **Fixed.** Such a match is dropped and recorded as incomplete coverage; the scan completes. Seen failing without it. | +| S5 | should-fix | `data/LICENSES/opengrep-binary-distribution.md` put the duties under LGPL-2.1 §6 rather than §4, and missed that the release binary is a PyInstaller bundle carrying GNU Readline, OpenSSL, certifi, chardet and CPython. | **Fixed.** The note now cites §4, lists the bundle's contents (verified by listing the unpacked bundle) and says which licences are still unverified (Readline's GPL-3.0 is the critic's memory). The owner decided on 2026-10-04: opengrep is an operator-installed prerequisite, so no Anvil artifact conveys the binary. | +| S6 | should-fix | The new corpora section split `THIRD-PARTY-LICENSES.md`'s tools table. | **Fixed.** | +| N1 | note | `anvil recall` exited 0 on a tree with nothing to scan. | **Fixed.** It records the empty plan as a problem and exits 3, as a repository scan does. | +| N2 | note | opengrep's partial parsing is not treated as incomplete. | **Kept.** The critic's five broken Java variants all still matched; on curl, about 140 of its 495 files parse in part on every scan. Recorded as a note on every scan. | +| N3 | note | Run-level `tool.extensions` strings (rule names, licence evidence) carry no trust label. | **Stated** in CONTRACT.md's 1.1.0 amendment: they are third-party text, never `anvil_generated`, and a consumer treats them as untrusted. | +| N4 | note | `anvil/reasoning` said "a parser named the enclosing function" for bandit's indentation reading and opengrep's class names. | **Fixed** in the vocabulary. | +| N5 | note | The spec harvest checked size with `Lstat` and then read without a limit. | **Fixed:** a bounded read. | + +## Conclusions that survived + +0xdea's rules (no per-file headers or attribution, MIT stands); the gosec-derived Go rules (byte-identical, +headers kept; 27 now, one of them derived by its companion rather than its own header; Apache-2.0 body from the release archive; §4(d) depends on gosec shipping no NOTICE, which was not +re-checked against its repository); nothing but rule YAML and licence bodies is vendored, and `c/`, `rules/lgpl*`, +`doc/` and `mappings/` are unreachable; `.bandit` discovery, files over opengrep's size limit, bandit syntax errors +and binary `.py` files are all recorded as incomplete; file names with a leading dash or a newline are safe because +targets are absolute; `anvil/reasoning` holds only vocabulary and integers; a missing tool, a wrong version, a bad +exit code, an empty or unparseable report and an empty plan all fail closed. + +## What this review does not cover + +It is one critic of the same model family. Two of its licence claims rest on memory (the find-sec-bugs prose +match, GNU Readline 7's licence) and were not checked against fetched sources. Every remaining Java, Scala and C# +rule still maps to a find-sec-bugs or security-code-scan rule id in GitLab's `mappings/`; GitLab labels their +companions as its own (MIT), which is the evidence the owner's criterion reads, and no text-level comparison against +the upstream analysers was made. One Java rule (`java/endpoint/rule-UnvalidatedRedirect.yml`) had a companion with +no licence header at all, so only the rule's MIT header spoke for it; the owner dropped it on 2026-10-04. diff --git a/eval/RUNNING.md b/eval/RUNNING.md index b59eecf..f506655 100644 --- a/eval/RUNNING.md +++ b/eval/RUNNING.md @@ -29,7 +29,7 @@ nothing else running unless noted. | advisory-permutation | `python -m anvil_eval.experiments.permutation run --model qwen3.5-2b --device CUDA0` and, in parallel, `... --model gemma-4-e2b-it --device CUDA1`; then `... permutation report` | RTX 4070 and RTX 4060, under 4 GB each | 0.7 s an answer on an idle machine; about 30 min each. CPU load from another job makes it about 10 times slower | `results/runs/advisory-permutation/.jsonl` (resumable), then `results/advisory-permutation.json` | | code-metrics-baseline | `python -m anvil_eval.experiments.baseline report` (after the permutation report) | CPU | features for the 175,797 training functions take 2.4 min once, then are cached | `results/code-metrics-baseline.json` | | prefill-sweep | `python -m anvil_eval.experiments.prefill run --model qwen3.5-2b --arms rtx4070 rtx4060 cpu12 tier-s cpu1` (and the same for gemma-4-e2b-it), then `... prefill report` | both GPUs, then the CPU alone; run it with nothing else on the machine | about 10 min per model | `results/runs/prefill-sweep/`, `results/prefill-sweep.json` | -| candidates-per-scan | `python -m anvil_eval.experiments.candidates run`, then `... candidates report` | CPU, all cores | about 15 min | `results/runs/candidates-per-scan/counts.json`, `results/candidates-per-scan.json` | +| candidates-per-scan | `python -m anvil_eval.experiments.candidates run`, then `... candidates report` | CPU, all cores | about 15 min before Phase 6, 3.5 min since (see below) | `results/runs/candidates-per-scan/counts.json`, `results/candidates-per-scan.json` | | encoder-round-trip | `python -m anvil_eval.encoder measure --pairs 5000`, then `... encoder report` (after the prefill report) | CPU, 4 threads | about 8 min | `results/runs/encoder-round-trip/timings.json`, `results/encoder-round-trip.json` | | patch-quality | `python -m anvil_eval.experiments.patch run --jdk 8u202= --jdk 17=`, then `... patch report` | both GPUs plus system RAM for the coder's experts | not yet run: it needs the coder (a 61 GB download), JDK 8 and 17, Maven and network access for Maven at build time | `results/runs/patch-quality/outcomes.jsonl`, `results/patch-quality.json` | @@ -42,6 +42,14 @@ A trial on a few pairs never writes into `results/`: `permutation run --limit 3 | advisory-permutation | 50 pairs per framing asked again with their own advisory and the prompt cache off: at most one verdict may change | | code-metrics-baseline | a pair-blind scorer has P-C exactly 0 and precision exactly 0.5, and the model compared with itself overlaps | | prefill-sweep | one CPU thread is clearly slower than four (ratio under 0.75), so no GPU leaked into a CPU arm | -| candidates-per-scan | a planted file per language family is each flagged, and a clean file is not | +| candidates-per-scan | a planted file per language family is each flagged, the same files under `tests/` are not, and a clean file is not | | encoder-round-trip | the worker refuses other paths and malformed bodies, and scores a duplicated pair identically | | patch-quality | a real canned fix is `verified_fixed` and a cosmetic one `exploit_still_triggers` on the synthetic C case | + +## Since Phase 6 + +The candidates-per-scan row measures through Lane B itself: `python -m anvil_eval.experiments.candidates run` +builds `anvil` from the checkout and drives `anvil recall` with the vendored rule pack (`data/rules`) and the pinned +tools, so the number recorded is the number a scan records. It took 3 min 24 s on 2026-10-03 (all cores). The +rule pack is regenerated from the owner's selection by `python -m anvil_eval.vendor_rules` (needs `python -m +anvil_eval.recall acquire` first); it writes nothing if any selected rule's licence is not MIT or Apache-2.0. diff --git a/eval/register.yaml b/eval/register.yaml index 361eb30..c65be81 100644 --- a/eval/register.yaml +++ b/eval/register.yaml @@ -58,6 +58,11 @@ # each with its artifact under eval/results/. The owner set their decisions by # the pre-registered bands and decided the gate (the `gate` block below). # patch-quality is deferred to Phase 7 by the owner; eight rows stay deferred. +# +# PHASE 6 (2026-10-03): candidates-per-scan was re-run through Lane B itself (anvil recall) on the +# owner's selection of that day (data/rules/selection.json): largest full scan 225 (curl, was +# 10,714), largest push 27 (Anvil, was 88), under both bars. The owner set the row's decision to +# PASS on 2026-10-04. version: 1 @@ -493,11 +498,13 @@ experiments: research/14-critique-and-gaps.md B1, restating the spine's figures: < 500 per full scan and < 50 per push result: - value: 10714 + value: 225 unit: "candidates per full scan, the largest of the sample repositories (per-push counts and every repository in the artifact)" - measured_at: "2026-10-03" + measured_at: "2026-10-04" artifact_path: "eval/results/candidates-per-scan.json" - decision: FAIL + # Set to PASS by the owner on 2026-10-04: under both bars on the owner's selection (largest full + # scan 225, largest push 27, control passed). It replaces the FAIL set on the first count, 10,714. + decision: PASS depends_on: [] deferred_reason: null owning_future_phase: null diff --git a/eval/results/candidates-per-scan.json b/eval/results/candidates-per-scan.json index e845de4..125656a 100644 --- a/eval/results/candidates-per-scan.json +++ b/eval/results/candidates-per-scan.json @@ -1,28 +1,42 @@ { "id": "candidates-per-scan", - "measured_at": "2026-10-03", - "value": 10714, + "measured_at": "2026-10-04", + "value": 225, "unit": "candidates per full scan, the largest of the sample repositories (per-push counts and every repository in the artifact)", "command": "python -m anvil_eval.experiments.candidates report", "git": { - "commit": "666698643f22fb1863e7c24cc20c2dbe29610cbc", - "dirty": true + "commit": "4d6ff85182540a15f5428f86d31bee330673b0c5", + "dirty": false }, "pins": { - "opengrep": "v1.26.0 (eval/tools/opengrep/MANIFEST.toml)", - "gitlab_sast_rules": { - "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", - "dirs": [ - "c", - "csharp", - "go", - "java", - "javascript", - "python", - "scala" - ] + "measured_through": "anvil recall, built from the commit in git", + "selection_sha256": "7b86bf58da7eaca7d0a19ec9a96fef3fe2b8ace672c4a8b7647e3ff5c236e729", + "selection_decided": "the owner, 2026-10-03", + "rule_files": 176, + "corpora": { + "gitlab-sast-rules": { + "commit": "53bf5cf6df3c51b6c02110f5a638b5e6213666cd", + "directories": [ + "csharp", + "go", + "java", + "javascript", + "python", + "scala" + ] + }, + "0xdea-semgrep-rules": { + "commit": "dae50da6e6e629750f1daec73a01ef9206a372ab", + "directories": [ + "rules/c" + ] + } + }, + "tools": { + "opengrep": "1.26.0", + "gosec": "2.29.0", + "bandit": "1.9.4" }, - "0xdea_semgrep_rules": "dae50da6e6e629750f1daec73a01ef9206a372ab", "gosec": { "version": "2.29.0", "url": "https://github.com/securego/gosec/releases/download/v2.29.0/gosec_2.29.0_linux_amd64.tar.gz", @@ -46,21 +60,33 @@ { "repo": "flask", "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", - "full_scan": 1090, + "full_scan": 9, "full_scan_by_tool": { - "opengrep": 8, - "bandit": 1082 + "bandit": 7, + "opengrep": 2 + }, + "full_scan_by_rule_top10": { + "bandit/B105@1.9.4": 2, + "bandit/B102@1.9.4": 1, + "bandit/B110@1.9.4": 1, + "bandit/B307@1.9.4": 1, + "bandit/B324@1.9.4": 1, + "bandit/B704@1.9.4": 1, + "gitlab-sast-rules/python_eval_rule-eval@359cce5bb9ec": 1, + "gitlab-sast-rules/python_exec_rule-exec-used@79e4e003d808": 1 }, "full_scan_by_cwe_top10": { - "CWE-703": 1061, - "CWE-259": 14, - "CWE-754": 5, - "CWE-79": 3, - "CWE-95": 2, + "CWE-259": 2, "CWE-78": 2, - "CWE-94": 2, - "CWE-327": 1 + "CWE-95": 2, + "CWE-327": 1, + "CWE-703": 1, + "CWE-79": 1 }, + "files_scanned": 24, + "files_excluded": 59, + "coverage_problems": [], + "partial_parses": 0, "pushes": [ { "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", @@ -90,7 +116,7 @@ { "commit": "05e9c6bd630ecf4ec0ec884b1fc7901663737bc7", "files_changed": 4, - "candidates": 1 + "candidates": 0 }, { "commit": "514fc6b3e8402e4c646d5284e97a4f0ab50a7c4b", @@ -100,7 +126,7 @@ { "commit": "8b4fd5d18611e63080b826ecfefe8659dba7d2e4", "files_changed": 2, - "candidates": 4 + "candidates": 0 }, { "commit": "6a2f545bfd8ed31e19066a299296917e034aca58", @@ -135,7 +161,7 @@ { "commit": "06ea505ce2b2042af26e96d35ebf159af7c0869d", "files_changed": 2, - "candidates": 7 + "candidates": 0 }, { "commit": "2ac89889f4cc330eabd50f295dcef02828522c69", @@ -164,28 +190,47 @@ } ], "per_push_median": 0, - "per_push_max": 7 + "per_push_max": 0 }, { "repo": "curl", "commit": "21983c2fca1e4ecf3393e94455b343d082b6f1c9", - "full_scan": 10714, + "full_scan": 225, "full_scan_by_tool": { - "opengrep": 10169, - "bandit": 545 + "opengrep": 225 + }, + "full_scan_by_rule_top10": { + "0xdea-semgrep-rules/raptor-integer-truncation@4987332556cd": 57, + "0xdea-semgrep-rules/raptor-write-into-stack-buffer@5be6610551c1": 49, + "0xdea-semgrep-rules/raptor-ret-stack-address@627ec7f5f0c1": 30, + "0xdea-semgrep-rules/raptor-mismatched-memory-management@a20305220299": 23, + "0xdea-semgrep-rules/raptor-off-by-one@d32d7d833064": 22, + "0xdea-semgrep-rules/raptor-integer-wraparound@c2f477b556b7": 17, + "0xdea-semgrep-rules/raptor-use-of-source-size-in-copy@938939df8655": 8, + "0xdea-semgrep-rules/raptor-incorrect-unsigned-comparison@549c7e8e3a64": 7, + "0xdea-semgrep-rules/raptor-insecure-api-signal@7029f954fe26": 5, + "0xdea-semgrep-rules/raptor-incorrect-use-of-sizeof@1e7b26fd2ff3": 3 }, "full_scan_by_cwe_top10": { - "CWE-546": 5249, - "CWE-676": 1584, - "CWE-480": 995, - "CWE-195": 543, - "CWE-703": 492, - "CWE-126": 483, - "CWE-120": 256, - "CWE-754": 162, - "CWE-478": 160, - "CWE-469": 128 + "CWE-197": 57, + "CWE-121": 49, + "CWE-562": 30, + "CWE-762": 23, + "CWE-193": 22, + "CWE-190": 17, + "CWE-806": 8, + "CWE-697": 7, + "CWE-364": 5, + "CWE-467": 3 }, + "files_scanned": 495, + "files_excluded": 606, + "coverage_problems": [ + "opengrep warn in /home/tsnyd/.cache/anvil-eval/recall/repos/curl/lib/vtls/schannel.c: Timeout when running raptor-unchecked-ret-malloc on /home/tsnyd/.cache/anvil-eval/recall/repos/curl/lib/vtls/schannel.c:", + "opengrep warn in /home/tsnyd/.cache/anvil-eval/recall/repos/curl/src/tool_doswin.c: Timeout when running raptor-integer-wraparound on /home/tsnyd/.cache/anvil-eval/recall/repos/curl/src/tool_doswin.c:", + "opengrep warn in /home/tsnyd/.cache/anvil-eval/recall/repos/curl/src/tool_doswin.c: Timeout when running raptor-unchecked-ret-malloc on /home/tsnyd/.cache/anvil-eval/recall/repos/curl/src/tool_doswin.c:" + ], + "partial_parses": 142, "pushes": [ { "commit": "21983c2fca1e4ecf3393e94455b343d082b6f1c9", @@ -260,7 +305,7 @@ { "commit": "978cce12adb7251b48a9dfa03a817df83f15cfaf", "files_changed": 1, - "candidates": 1 + "candidates": 0 }, { "commit": "318bd978ea2d36ace36b42da2174b79c3abd89a3", @@ -289,14 +334,19 @@ } ], "per_push_median": 0, - "per_push_max": 1 + "per_push_max": 0 }, { "repo": "spring-petclinic", "commit": "500158f732419217507c7656904b8e6aa1bcc0d6", "full_scan": 0, "full_scan_by_tool": {}, + "full_scan_by_rule_top10": {}, "full_scan_by_cwe_top10": {}, + "files_scanned": 30, + "files_excluded": 20, + "coverage_problems": [], + "partial_parses": 0, "pushes": [ { "commit": "500158f732419217507c7656904b8e6aa1bcc0d6", @@ -405,15 +455,22 @@ { "repo": "express", "commit": "7ef98448f8b38099ab1ded55e458538ad47a51e7", - "full_scan": 8, + "full_scan": 2, "full_scan_by_tool": { - "opengrep": 8 + "opengrep": 2 + }, + "full_scan_by_rule_top10": { + "gitlab-sast-rules/javascript_pathtraversal_rule-non-literal-fs-filename@c0d8dcf2117f": 1, + "gitlab-sast-rules/javascript_require_rule-non-literal-require@b81110bbc629": 1 }, "full_scan_by_cwe_top10": { - "CWE-22": 4, - "CWE-95": 3, - "CWE-208": 1 + "CWE-22": 1, + "CWE-95": 1 }, + "files_scanned": 7, + "files_excluded": 134, + "coverage_problems": [], + "partial_parses": 0, "pushes": [ { "commit": "7ef98448f8b38099ab1ded55e458538ad47a51e7", @@ -522,54 +579,70 @@ { "repo": "anvil", "commit": "1d8657e889cdf292b1ee792ceb36913f6d8539d2", - "full_scan": 159, + "full_scan": 79, "full_scan_by_tool": { - "opengrep": 31, - "bandit": 86, - "gosec": 42 + "bandit": 9, + "gosec": 56, + "opengrep": 14 + }, + "full_scan_by_rule_top10": { + "gosec/G304@2.29.0": 22, + "gosec/G115@2.29.0": 10, + "gitlab-sast-rules/go_file-permissions_rule-mkdir@87767da1f29a": 6, + "gitlab-sast-rules/go_sql_rule-concat-sqli@3997ad3edc7f": 6, + "gosec/G204@2.29.0": 6, + "gosec/G301@2.29.0": 6, + "gosec/G101@2.29.0": 5, + "gosec/G202@2.29.0": 4, + "bandit/B603@1.9.4": 3, + "bandit/B105@1.9.4": 2 }, "full_scan_by_cwe_top10": { - "CWE-703": 74, "CWE-22": 24, - "CWE-78": 16, - "CWE-89": 10, + "CWE-78": 11, "CWE-190": 10, - "CWE-732": 7, + "CWE-89": 10, "CWE-276": 7, + "CWE-732": 7, "CWE-798": 5, "CWE-259": 2, - "CWE-327": 1 + "CWE-327": 1, + "CWE-338": 1 }, + "files_scanned": 141, + "files_excluded": 101, + "coverage_problems": [], + "partial_parses": 0, "pushes": [ { "commit": "1d8657e889cdf292b1ee792ceb36913f6d8539d2", "files_changed": 65, - "candidates": 12 + "candidates": 7 }, { "commit": "009b468a68f4160e4c35961166657c6d7290d781", "files_changed": 22, - "candidates": 3 + "candidates": 0 }, { "commit": "17e86d9db76442f02c4d2f4e13aeb2f8c486da3d", "files_changed": 141, - "candidates": 1 + "candidates": 0 }, { "commit": "fe18b231abc9e0cfe6df2f11536d2d976fda24b7", "files_changed": 227, - "candidates": 61 + "candidates": 27 }, { "commit": "67b6daa86ebc605d6ad010f808d95c3a84d6bcb5", "files_changed": 29, - "candidates": 1 + "candidates": 0 }, { "commit": "eb28b4b5c02ea838725bf993755ab00e1d40a9da", "files_changed": 8, - "candidates": 1 + "candidates": 0 }, { "commit": "4e7da7641005d251be48553f3127b9543d51211e", @@ -579,32 +652,32 @@ { "commit": "2db083e8a95451b8a33d6e9d5b157539af142d39", "files_changed": 35, - "candidates": 43 + "candidates": 18 }, { "commit": "3d608562b309919510de1bbe2688f054f15cf347", "files_changed": 31, - "candidates": 6 + "candidates": 5 }, { "commit": "aac11c4e213bfb6b69227773134f1705e27cd562", "files_changed": 21, - "candidates": 10 + "candidates": 1 }, { "commit": "fe9ec3e90b308f0546f8548debf2c65406495811", "files_changed": 28, - "candidates": 3 + "candidates": 2 }, { "commit": "02e3404a9cfe65ec8afe51d91036b5367be2bc4e", "files_changed": 17, - "candidates": 6 + "candidates": 3 }, { "commit": "a1230513fe1b34ec5fa4a6564f928a87feffb261", "files_changed": 21, - "candidates": 17 + "candidates": 8 }, { "commit": "5591198929e5f06decee95f9b14ac1ca64d8cf9d", @@ -614,7 +687,7 @@ { "commit": "167853edc02216c18933c665e3159781c50e8184", "files_changed": 30, - "candidates": 88 + "candidates": 8 }, { "commit": "cd6471139ee16db2721e55d65f5d55847cc32c95", @@ -637,39 +710,54 @@ "candidates": 0 } ], - "per_push_median": 3, - "per_push_max": 88 + "per_push_median": 0, + "per_push_max": 27 } ], "largest_full_scan": { "repo": "curl", - "candidates": 10714 + "candidates": 225 }, - "largest_push": 88, + "largest_push": 27, "bars": { "full_scan": 500, "per_push": 50 }, - "within_bars": false + "within_bars": true, + "previous": { + "measured_at": "2026-10-03", + "value": 10714, + "git_commit": "666698643f22fb1863e7c24cc20c2dbe29610cbc", + "what": "the whole federation, before the owner's selection: GitLab's seven allowlisted directories including c/, 0xdea's rules including rules/noisy, bandit B101, no path exclusions; counted by the Python runners this module then held", + "full_scan": { + "curl": 10714, + "flask": 1090, + "anvil": 159, + "express": 8, + "spring-petclinic": 0 + }, + "largest_push": 88 + } }, "negative_control": { - "description": "a planted file per language family must each yield a candidate, and a clean file none", - "expected": "every planted file flagged; no candidate in the clean file", + "description": "a planted file per language family must each yield a candidate, the same files under tests/ none, and a clean file none", + "expected": "every planted file flagged; nothing under tests/; no candidate in the clean file", "observed": { "planted_files": [ - "BadExec.java", + "BadCipher.java", "bad_eval.js", "bad_eval.py", "bad_exec.go", "bad_strcpy.c" ], "planted_flagged": [ - "BadExec.java", + "BadCipher.java", "bad_eval.js", "bad_eval.py", "bad_exec.go", "bad_strcpy.c" ], + "flagged_under_tests": [], "by_tool": [ [ "bandit", @@ -681,7 +769,7 @@ ], [ "opengrep", - "BadExec.java" + "BadCipher.java" ], [ "opengrep", diff --git a/eval/results/runs/candidates-per-scan/counts.json b/eval/results/runs/candidates-per-scan/counts.json index ecd662a..ae35c42 100644 --- a/eval/results/runs/candidates-per-scan/counts.json +++ b/eval/results/runs/candidates-per-scan/counts.json @@ -3,21 +3,33 @@ { "repo": "flask", "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", - "full_scan": 1090, + "full_scan": 9, "full_scan_by_tool": { - "opengrep": 8, - "bandit": 1082 + "bandit": 7, + "opengrep": 2 + }, + "full_scan_by_rule_top10": { + "bandit/B105@1.9.4": 2, + "bandit/B102@1.9.4": 1, + "bandit/B110@1.9.4": 1, + "bandit/B307@1.9.4": 1, + "bandit/B324@1.9.4": 1, + "bandit/B704@1.9.4": 1, + "gitlab-sast-rules/python_eval_rule-eval@359cce5bb9ec": 1, + "gitlab-sast-rules/python_exec_rule-exec-used@79e4e003d808": 1 }, "full_scan_by_cwe_top10": { - "CWE-703": 1061, - "CWE-259": 14, - "CWE-754": 5, - "CWE-79": 3, - "CWE-95": 2, + "CWE-259": 2, "CWE-78": 2, - "CWE-94": 2, - "CWE-327": 1 + "CWE-95": 2, + "CWE-327": 1, + "CWE-703": 1, + "CWE-79": 1 }, + "files_scanned": 24, + "files_excluded": 59, + "coverage_problems": [], + "partial_parses": 0, "pushes": [ { "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", @@ -47,7 +59,7 @@ { "commit": "05e9c6bd630ecf4ec0ec884b1fc7901663737bc7", "files_changed": 4, - "candidates": 1 + "candidates": 0 }, { "commit": "514fc6b3e8402e4c646d5284e97a4f0ab50a7c4b", @@ -57,7 +69,7 @@ { "commit": "8b4fd5d18611e63080b826ecfefe8659dba7d2e4", "files_changed": 2, - "candidates": 4 + "candidates": 0 }, { "commit": "6a2f545bfd8ed31e19066a299296917e034aca58", @@ -92,7 +104,7 @@ { "commit": "06ea505ce2b2042af26e96d35ebf159af7c0869d", "files_changed": 2, - "candidates": 7 + "candidates": 0 }, { "commit": "2ac89889f4cc330eabd50f295dcef02828522c69", @@ -121,28 +133,47 @@ } ], "per_push_median": 0, - "per_push_max": 7 + "per_push_max": 0 }, { "repo": "curl", "commit": "21983c2fca1e4ecf3393e94455b343d082b6f1c9", - "full_scan": 10714, + "full_scan": 225, "full_scan_by_tool": { - "opengrep": 10169, - "bandit": 545 + "opengrep": 225 + }, + "full_scan_by_rule_top10": { + "0xdea-semgrep-rules/raptor-integer-truncation@4987332556cd": 57, + "0xdea-semgrep-rules/raptor-write-into-stack-buffer@5be6610551c1": 49, + "0xdea-semgrep-rules/raptor-ret-stack-address@627ec7f5f0c1": 30, + "0xdea-semgrep-rules/raptor-mismatched-memory-management@a20305220299": 23, + "0xdea-semgrep-rules/raptor-off-by-one@d32d7d833064": 22, + "0xdea-semgrep-rules/raptor-integer-wraparound@c2f477b556b7": 17, + "0xdea-semgrep-rules/raptor-use-of-source-size-in-copy@938939df8655": 8, + "0xdea-semgrep-rules/raptor-incorrect-unsigned-comparison@549c7e8e3a64": 7, + "0xdea-semgrep-rules/raptor-insecure-api-signal@7029f954fe26": 5, + "0xdea-semgrep-rules/raptor-incorrect-use-of-sizeof@1e7b26fd2ff3": 3 }, "full_scan_by_cwe_top10": { - "CWE-546": 5249, - "CWE-676": 1584, - "CWE-480": 995, - "CWE-195": 543, - "CWE-703": 492, - "CWE-126": 483, - "CWE-120": 256, - "CWE-754": 162, - "CWE-478": 160, - "CWE-469": 128 + "CWE-197": 57, + "CWE-121": 49, + "CWE-562": 30, + "CWE-762": 23, + "CWE-193": 22, + "CWE-190": 17, + "CWE-806": 8, + "CWE-697": 7, + "CWE-364": 5, + "CWE-467": 3 }, + "files_scanned": 495, + "files_excluded": 606, + "coverage_problems": [ + "opengrep warn in /home/tsnyd/.cache/anvil-eval/recall/repos/curl/lib/vtls/schannel.c: Timeout when running raptor-unchecked-ret-malloc on /home/tsnyd/.cache/anvil-eval/recall/repos/curl/lib/vtls/schannel.c:", + "opengrep warn in /home/tsnyd/.cache/anvil-eval/recall/repos/curl/src/tool_doswin.c: Timeout when running raptor-integer-wraparound on /home/tsnyd/.cache/anvil-eval/recall/repos/curl/src/tool_doswin.c:", + "opengrep warn in /home/tsnyd/.cache/anvil-eval/recall/repos/curl/src/tool_doswin.c: Timeout when running raptor-unchecked-ret-malloc on /home/tsnyd/.cache/anvil-eval/recall/repos/curl/src/tool_doswin.c:" + ], + "partial_parses": 142, "pushes": [ { "commit": "21983c2fca1e4ecf3393e94455b343d082b6f1c9", @@ -217,7 +248,7 @@ { "commit": "978cce12adb7251b48a9dfa03a817df83f15cfaf", "files_changed": 1, - "candidates": 1 + "candidates": 0 }, { "commit": "318bd978ea2d36ace36b42da2174b79c3abd89a3", @@ -246,14 +277,19 @@ } ], "per_push_median": 0, - "per_push_max": 1 + "per_push_max": 0 }, { "repo": "spring-petclinic", "commit": "500158f732419217507c7656904b8e6aa1bcc0d6", "full_scan": 0, "full_scan_by_tool": {}, + "full_scan_by_rule_top10": {}, "full_scan_by_cwe_top10": {}, + "files_scanned": 30, + "files_excluded": 20, + "coverage_problems": [], + "partial_parses": 0, "pushes": [ { "commit": "500158f732419217507c7656904b8e6aa1bcc0d6", @@ -362,15 +398,22 @@ { "repo": "express", "commit": "7ef98448f8b38099ab1ded55e458538ad47a51e7", - "full_scan": 8, + "full_scan": 2, "full_scan_by_tool": { - "opengrep": 8 + "opengrep": 2 + }, + "full_scan_by_rule_top10": { + "gitlab-sast-rules/javascript_pathtraversal_rule-non-literal-fs-filename@c0d8dcf2117f": 1, + "gitlab-sast-rules/javascript_require_rule-non-literal-require@b81110bbc629": 1 }, "full_scan_by_cwe_top10": { - "CWE-22": 4, - "CWE-95": 3, - "CWE-208": 1 + "CWE-22": 1, + "CWE-95": 1 }, + "files_scanned": 7, + "files_excluded": 134, + "coverage_problems": [], + "partial_parses": 0, "pushes": [ { "commit": "7ef98448f8b38099ab1ded55e458538ad47a51e7", @@ -479,54 +522,70 @@ { "repo": "anvil", "commit": "1d8657e889cdf292b1ee792ceb36913f6d8539d2", - "full_scan": 159, + "full_scan": 79, "full_scan_by_tool": { - "opengrep": 31, - "bandit": 86, - "gosec": 42 + "bandit": 9, + "gosec": 56, + "opengrep": 14 + }, + "full_scan_by_rule_top10": { + "gosec/G304@2.29.0": 22, + "gosec/G115@2.29.0": 10, + "gitlab-sast-rules/go_file-permissions_rule-mkdir@87767da1f29a": 6, + "gitlab-sast-rules/go_sql_rule-concat-sqli@3997ad3edc7f": 6, + "gosec/G204@2.29.0": 6, + "gosec/G301@2.29.0": 6, + "gosec/G101@2.29.0": 5, + "gosec/G202@2.29.0": 4, + "bandit/B603@1.9.4": 3, + "bandit/B105@1.9.4": 2 }, "full_scan_by_cwe_top10": { - "CWE-703": 74, "CWE-22": 24, - "CWE-78": 16, - "CWE-89": 10, + "CWE-78": 11, "CWE-190": 10, - "CWE-732": 7, + "CWE-89": 10, "CWE-276": 7, + "CWE-732": 7, "CWE-798": 5, "CWE-259": 2, - "CWE-327": 1 + "CWE-327": 1, + "CWE-338": 1 }, + "files_scanned": 141, + "files_excluded": 101, + "coverage_problems": [], + "partial_parses": 0, "pushes": [ { "commit": "1d8657e889cdf292b1ee792ceb36913f6d8539d2", "files_changed": 65, - "candidates": 12 + "candidates": 7 }, { "commit": "009b468a68f4160e4c35961166657c6d7290d781", "files_changed": 22, - "candidates": 3 + "candidates": 0 }, { "commit": "17e86d9db76442f02c4d2f4e13aeb2f8c486da3d", "files_changed": 141, - "candidates": 1 + "candidates": 0 }, { "commit": "fe18b231abc9e0cfe6df2f11536d2d976fda24b7", "files_changed": 227, - "candidates": 61 + "candidates": 27 }, { "commit": "67b6daa86ebc605d6ad010f808d95c3a84d6bcb5", "files_changed": 29, - "candidates": 1 + "candidates": 0 }, { "commit": "eb28b4b5c02ea838725bf993755ab00e1d40a9da", "files_changed": 8, - "candidates": 1 + "candidates": 0 }, { "commit": "4e7da7641005d251be48553f3127b9543d51211e", @@ -536,32 +595,32 @@ { "commit": "2db083e8a95451b8a33d6e9d5b157539af142d39", "files_changed": 35, - "candidates": 43 + "candidates": 18 }, { "commit": "3d608562b309919510de1bbe2688f054f15cf347", "files_changed": 31, - "candidates": 6 + "candidates": 5 }, { "commit": "aac11c4e213bfb6b69227773134f1705e27cd562", "files_changed": 21, - "candidates": 10 + "candidates": 1 }, { "commit": "fe9ec3e90b308f0546f8548debf2c65406495811", "files_changed": 28, - "candidates": 3 + "candidates": 2 }, { "commit": "02e3404a9cfe65ec8afe51d91036b5367be2bc4e", "files_changed": 17, - "candidates": 6 + "candidates": 3 }, { "commit": "a1230513fe1b34ec5fa4a6564f928a87feffb261", "files_changed": 21, - "candidates": 17 + "candidates": 8 }, { "commit": "5591198929e5f06decee95f9b14ac1ca64d8cf9d", @@ -571,7 +630,7 @@ { "commit": "167853edc02216c18933c665e3159781c50e8184", "files_changed": 30, - "candidates": 88 + "candidates": 8 }, { "commit": "cd6471139ee16db2721e55d65f5d55847cc32c95", @@ -594,25 +653,26 @@ "candidates": 0 } ], - "per_push_median": 3, - "per_push_max": 88 + "per_push_median": 0, + "per_push_max": 27 } ], "control": { "planted_files": [ - "BadExec.java", + "BadCipher.java", "bad_eval.js", "bad_eval.py", "bad_exec.go", "bad_strcpy.c" ], "planted_flagged": [ - "BadExec.java", + "BadCipher.java", "bad_eval.js", "bad_eval.py", "bad_exec.go", "bad_strcpy.c" ], + "flagged_under_tests": [], "by_tool": [ [ "bandit", @@ -624,7 +684,7 @@ ], [ "opengrep", - "BadExec.java" + "BadCipher.java" ], [ "opengrep", diff --git a/eval/src/anvil_eval/experiments/candidates.py b/eval/src/anvil_eval/experiments/candidates.py index f545c95..2d9541a 100644 --- a/eval/src/anvil_eval/experiments/candidates.py +++ b/eval/src/anvil_eval/experiments/candidates.py @@ -1,23 +1,28 @@ """Candidates per scan (register row candidates-per-scan, plan node candidatecount). -A permanent instrument: run the recall federation (opengrep over GitLab's sast-rules and 0xdea's -C/C++ rules, gosec on Go, bandit on Python) over a pinned repository and count candidates. One -candidate is one (file, line, CWE) after de-duplication across tools; a finding without a CWE -keeps its rule id in that slot. +A permanent instrument. Since Phase 6 it measures through Lane B itself: ``anvil recall``, built +from this checkout, runs the vendored rule pack (``data/rules``, the owner's selection of +2026-10-03) and the native analysers exactly as a repository scan does, and prints the candidates. +There is one implementation of the selection, the exclusions and the de-duplication, and it is the +one that ships; this module only drives it and counts. * Full scan: the whole checkout at the pinned commit. * Per push: each of the most recent first-parent commits (up to 20), scanning the files that commit touched and keeping only candidates on lines it added or changed. -Each tool is a subprocess. A missing tool is a refusal and a failed tool is an error; neither is -ever a count of zero, because a zero from a broken install and a zero from a clean repository -must not look alike (that is how a two-rule corpus measured nothing). +A missing tool is a refusal and a failed tool is an error; neither is ever a count of zero. +``anvil recall`` exits 4 for the first, and its JSON lists coverage problems (a file a tool was +given and did not scan, a rule that timed out), which the artifact records per repository. -Negative control: the federation runs over a planted fixture with one known-bad file per -language family and over a clean file. Every planted file must yield a candidate and the clean -file none; otherwise the artifact does not validate. The Go file and its go.mod are stored with a -``.txt`` suffix, because ``eval/`` holds no Go source (``tests/test_scaffold.py``), and are +Negative control: Lane B runs over a planted fixture with one known-bad file per language family, +the same files again under a ``tests/`` directory, and a clean file. Every planted file must yield +a candidate, nothing under ``tests/`` may (the selection excludes test trees), and the clean file +must yield none; otherwise the artifact does not validate. The Go file and its go.mod are stored +with a ``.txt`` suffix, because ``eval/`` holds no Go source (``tests/test_scaffold.py``), and are materialised into a temporary directory for the scan. + +The measurement before the narrowing (10,714 candidates on curl, 2026-10-03, commit 66669864) is +carried in the artifact as ``previous``; its raw run is in git history. """ from __future__ import annotations @@ -29,8 +34,7 @@ import subprocess import sys import tempfile -from collections.abc import Iterable -from dataclasses import asdict, dataclass +from dataclasses import dataclass from pathlib import Path from anvil_eval import EVAL_ROOT, REPO_ROOT, RESULTS_DIR, recall, results @@ -41,140 +45,70 @@ FIXTURES = EVAL_ROOT / "tests" / "fixtures" / "recall" ANVIL_COMMIT = "1d8657e889cdf292b1ee792ceb36913f6d8539d2" # main after Phase 4 (#18) GO = Path.home() / "sdk" / "go1.26.5" / "bin" - -C_EXT = {".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"} -SCANNED_EXT = C_EXT | {".py", ".go", ".java", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", - ".cs", ".scala"} +RULES = REPO_ROOT / "data" / "rules" + +#: The measurement this instrument replaced on 2026-10-03, before the owner's narrowing. +PREVIOUS = { + "measured_at": "2026-10-03", + "value": 10714, + "git_commit": "666698643f22fb1863e7c24cc20c2dbe29610cbc", + "what": "the whole federation, before the owner's selection: GitLab's seven allowlisted " + "directories including c/, 0xdea's rules including rules/noisy, bandit B101, no " + "path exclusions; counted by the Python runners this module then held", + "full_scan": {"curl": 10714, "flask": 1090, "anvil": 159, "express": 8, + "spring-petclinic": 0}, + "largest_push": 88, +} class ToolMissing(RuntimeError): - """A recall tool is not installed; the scan is refused rather than counted as zero.""" + """A recall tool or the rule pack is not installed; the scan is refused, never zero.""" class ToolFailed(RuntimeError): - """A recall tool ran and failed.""" + """``anvil recall`` ran and failed.""" @dataclass(frozen=True) class Candidate: tool: str - rule: str + rule: str # versioned, as the record carries it path: str # relative to the scanned root line: int cwe: str - @property - def key(self) -> tuple[str, int, str]: - return (self.path, self.line, self.cwe or self.rule) - - -def _cwe(raw) -> str: - if isinstance(raw, list): - raw = raw[0] if raw else "" - if isinstance(raw, dict): - raw = raw.get("id", "") - m = re.search(r"(?i)cwe-?(\d+)", str(raw)) or re.fullmatch(r"\s*(\d+)\s*", str(raw)) - return f"CWE-{int(m.group(1))}" if m else "" # 'CWE-078' and '78' are one CWE - -def _need(path: Path, name: str) -> Path: - if not path.is_file() or not os.access(path, os.X_OK): - raise ToolMissing(f"{name} is not installed at {path}; run python -m anvil_eval.recall " - "acquire") - return path +class Anvil: + """``anvil recall``, built from this checkout and configured with the pinned tools.""" + def __init__(self, workdir: Path): + self.bin = workdir / "anvil" + subprocess.run([str(GO / "go"), "build", "-o", str(self.bin), "./cmd/anvil"], check=True, + cwd=REPO_ROOT, env=dict(os.environ, GOTOOLCHAIN="local", CGO_ENABLED="0")) + self.config = workdir / "anvil.yml" + self.config.write_text( + "version: 1\n" + f"stateDir: {workdir / 'state'}\n" + "recall:\n" + f" rules: {RULES}\n" + f" opengrep: {recall.opengrep_bin()}\n" + f" gosec: {recall.RECALL_BIN / 'gosec'}\n" + f" bandit: {recall.RECALL_VENV / 'bin' / 'bandit'}\n" + f" goBin: {GO}\n") -def run_opengrep(root: Path, targets: list[Path], has_c: bool) -> list[Candidate]: - exe = _need(recall.opengrep_bin(), "opengrep") - configs = [recall.GITLAB.path / d for d in recall.GITLAB_RULE_DIRS] - if has_c: - configs.append(recall.OXDEA.path / "rules") - with tempfile.TemporaryDirectory() as tmp: - out = Path(tmp) / "out.json" - cmd = [str(exe), "scan", "--json", "--output", str(out), "--quiet", "--no-git-ignore"] - for c in configs: - cmd += ["--config", str(c)] - proc = subprocess.run(cmd + [str(t) for t in targets], capture_output=True, text=True, - cwd=root) - if proc.returncode not in (0, 1) or not out.is_file(): - raise ToolFailed(f"opengrep exited {proc.returncode}: {proc.stderr[-800:]}") - doc = json.loads(out.read_text()) - return [Candidate("opengrep", r["check_id"].rsplit(".", 1)[-1], - os.path.relpath((root / r["path"]).resolve(), root.resolve()), - int(r["start"]["line"]), _cwe(r["extra"].get("metadata", {}).get("cwe"))) - for r in doc["results"]] - - -def run_gosec(root: Path, packages: list[str]) -> list[Candidate]: - exe = _need(recall.RECALL_BIN / "gosec", "gosec") - env = dict(os.environ, PATH=f"{GO}:{os.environ.get('PATH', '')}", GOTOOLCHAIN="local", - GOFLAGS="-mod=mod") - with tempfile.TemporaryDirectory() as tmp: - out = Path(tmp) / "out.json" - # Never -quiet: with it, gosec writes nothing at all for a package with no findings - # (seen on 2026-10-03), and silence must never be read as a count. - proc = subprocess.run([str(exe), "-fmt=json", f"-out={out}", "-no-fail", - *packages], capture_output=True, text=True, cwd=root, env=env) - if proc.returncode != 0 or not out.is_file(): - raise ToolFailed(f"gosec exited {proc.returncode}: {proc.stderr[-800:]}") - doc = json.loads(out.read_text()) - if doc.get("Golang errors"): - raise ToolFailed(f"gosec could not load packages: {list(doc['Golang errors'])[:3]}") - return [Candidate("gosec", i["rule_id"], os.path.relpath(i["file"], root.resolve()), - int(str(i["line"]).split("-")[0]), _cwe(i.get("cwe"))) - for i in doc.get("Issues") or []] - - -def run_bandit(root: Path, targets: list[Path]) -> list[Candidate]: - exe = _need(recall.RECALL_VENV / "bin" / "bandit", "bandit") - with tempfile.TemporaryDirectory() as tmp: - out = Path(tmp) / "out.json" - proc = subprocess.run([str(exe), "-r", "-q", "-f", "json", "-o", str(out), - *[str(t) for t in targets]], capture_output=True, text=True, - cwd=root) - if proc.returncode not in (0, 1) or not out.is_file(): - raise ToolFailed(f"bandit exited {proc.returncode}: {proc.stderr[-800:]}") - doc = json.loads(out.read_text()) - if doc.get("errors"): - raise ToolFailed(f"bandit errors: {doc['errors'][:3]}") - return [Candidate("bandit", r["test_id"], os.path.relpath((root / r["filename"]).resolve(), - root.resolve()), - int(r["line_number"]), _cwe(r.get("issue_cwe"))) - for r in doc["results"]] - - -def scan(root: Path, files: Iterable[Path] | None = None) -> list[Candidate]: - """Every tool the tree calls for, over ``files`` (all tracked files when None).""" - root = root.resolve() - if files is None: - listed = subprocess.run(["git", "-C", str(root), "ls-files"], capture_output=True, - text=True) - names = listed.stdout.split() if listed.returncode == 0 else [ - str(p.relative_to(root)) for p in root.rglob("*") if p.is_file()] - files = [root / n for n in names] - whole = True - else: - whole = False - files = [f for f in files if f.suffix in SCANNED_EXT and f.is_file()] - if not files: - return [] - out = run_opengrep(root, [root] if whole else files, any(f.suffix in C_EXT for f in files)) - py = [f for f in files if f.suffix == ".py"] - if py: - out += run_bandit(root, [root] if whole else py) - # gosec skips test files by default; leave them out of its package list to match. - go = [f for f in files if f.suffix == ".go" and not f.name.endswith("_test.go")] - if go and (root / "go.mod").is_file(): - pkgs = ["./..."] if whole else sorted({"./" + os.path.relpath(f.parent, root) for f in go}) - out += run_gosec(root, pkgs) - return out + def recall(self, root: Path, files: list[str] | None = None) -> dict: + proc = subprocess.run([str(self.bin), "recall", "--config", str(self.config), str(root), + *(files or [])], capture_output=True, text=True) + if proc.returncode == 4: + raise ToolMissing(proc.stderr.strip()) + if proc.returncode not in (0, 1, 3): + raise ToolFailed(f"anvil recall exited {proc.returncode}: {proc.stderr[-800:]}") + return json.loads(proc.stdout) -def dedupe(cands: list[Candidate]) -> list[Candidate]: - seen: dict[tuple, Candidate] = {} - for c in cands: - seen.setdefault(c.key, c) - return list(seen.values()) +def candidates_of(doc: dict) -> list[Candidate]: + return [Candidate(c["tool"], c["ruleIdVersioned"], c["path"], c["startLine"], c["cwe"]) + for c in doc["candidates"]] def changed_lines(root: Path, commit: str) -> dict[str, set[int]]: @@ -193,7 +127,7 @@ def changed_lines(root: Path, commit: str) -> dict[str, set[int]]: return {p: ls for p, ls in out.items() if ls} -def per_push(root: Path, pushes: int) -> list[dict]: +def per_push(anvil: Anvil, root: Path, pushes: int) -> list[dict]: head = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip() commits = subprocess.run(["git", "-C", str(root), "rev-list", "--first-parent", @@ -204,7 +138,7 @@ def per_push(root: Path, pushes: int) -> list[dict]: for c in commits: subprocess.run(["git", "-C", str(root), "checkout", "-q", "--detach", c], check=True) lines = changed_lines(root, c) - found = dedupe(scan(root, [root / p for p in lines])) + found = candidates_of(anvil.recall(root, sorted(lines))) if lines else [] kept = [f for f in found if f.line in lines.get(f.path, ())] rows.append({"commit": c, "files_changed": len(lines), "candidates": len(kept)}) finally: @@ -212,33 +146,36 @@ def per_push(root: Path, pushes: int) -> list[dict]: return rows -def measure_repo(name: str, root: Path, pushes: int) -> dict: - full = dedupe(scan(root)) - by_tool: dict[str, int] = {} - for c in full: - by_tool[c.tool] = by_tool.get(c.tool, 0) + 1 - push = per_push(root, pushes) +def _top(values: list[str], n: int = 10) -> dict[str, int]: + tally: dict[str, int] = {} + for v in values: + tally[v] = tally.get(v, 0) + 1 + return dict(sorted(tally.items(), key=lambda kv: (-kv[1], kv[0]))[:n]) + + +def measure_repo(anvil: Anvil, name: str, root: Path, pushes: int) -> dict: + doc = anvil.recall(root) + full = candidates_of(doc) + push = per_push(anvil, root, pushes) counts = sorted(p["candidates"] for p in push) return { "repo": name, "commit": subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip(), "full_scan": len(full), - "full_scan_by_tool": by_tool, - "full_scan_by_cwe_top10": _top(full), + "full_scan_by_tool": doc["byTool"], + "full_scan_by_rule_top10": _top([c.rule for c in full]), + "full_scan_by_cwe_top10": _top([c.cwe for c in full]), + "files_scanned": doc["files"], + "files_excluded": doc["excluded"], + "coverage_problems": doc["problems"], + "partial_parses": doc["partialParses"], "pushes": push, "per_push_median": counts[len(counts) // 2] if counts else 0, "per_push_max": counts[-1] if counts else 0, } -def _top(cands: list[Candidate]) -> dict[str, int]: - tally: dict[str, int] = {} - for c in cands: - tally[c.cwe or "none"] = tally.get(c.cwe or "none", 0) + 1 - return dict(sorted(tally.items(), key=lambda kv: -kv[1])[:10]) - - def materialise(src: Path, dest: Path) -> Path: """Copy a fixture directory, dropping the ``.txt`` that keeps Go sources out of eval/.""" dest.mkdir(parents=True, exist_ok=True) @@ -248,18 +185,28 @@ def materialise(src: Path, dest: Path) -> Path: return dest -def control() -> dict: +def control(anvil: Anvil) -> dict: with tempfile.TemporaryDirectory() as tmp: planted = materialise(FIXTURES / "planted", Path(tmp) / "planted") - found = dedupe(scan(planted, sorted(planted.iterdir()))) - files = sorted(p.name for p in planted.iterdir() if p.suffix in SCANNED_EXT) + # The same planted files under tests/: the selection excludes test trees, so none of + # these may be flagged. The Go module stays at the root. + tests = planted / "tests" + tests.mkdir() + for f in planted.iterdir(): + if f.is_file() and f.suffix in (".py", ".js", ".java", ".c"): + (tests / f.name).write_bytes(f.read_bytes()) + found = candidates_of(anvil.recall(planted)) + files = sorted(p.name for p in planted.iterdir() + if p.is_file() and p.suffix in (".py", ".js", ".java", ".c", ".go")) clean_dir = materialise(FIXTURES / "clean", Path(tmp) / "clean") - clean = dedupe(scan(clean_dir, sorted(clean_dir.iterdir()))) + clean = candidates_of(anvil.recall(clean_dir)) hit = {c.path for c in found} - return {"planted_files": files, "planted_flagged": sorted(hit), + in_tests = sorted(c.path for c in found if c.path.startswith("tests/")) + return {"planted_files": files, "planted_flagged": sorted(hit - set(in_tests)), + "flagged_under_tests": in_tests, "by_tool": sorted({(c.tool, c.path) for c in found}), - "clean_candidates": [asdict(c) for c in clean], - "passed": set(files) <= hit and not clean} + "clean_candidates": [c.__dict__ for c in clean], + "passed": set(files) <= hit and not in_tests and not clean} def anvil_checkout() -> Path: @@ -274,13 +221,17 @@ def run() -> Path: RUNS_DIR.mkdir(parents=True, exist_ok=True) targets = [(p.name, p.path, p.commit) for p in recall.REPOS] targets.append(("anvil", anvil_checkout(), ANVIL_COMMIT)) - rows = [] - for name, path, commit in targets: - # An interrupted run can leave a checkout at a push commit: start from the pin. - subprocess.run(["git", "-C", str(path), "checkout", "-q", "--detach", commit], check=True) - rows.append(measure_repo(name, path, recall.PUSHES)) + with tempfile.TemporaryDirectory() as tmp: + anvil = Anvil(Path(tmp)) + rows = [] + for name, path, commit in targets: + # An interrupted run can leave a checkout at a push commit: start from the pin. + subprocess.run(["git", "-C", str(path), "checkout", "-q", "--detach", commit], + check=True) + rows.append(measure_repo(anvil, name, path, recall.PUSHES)) + ctl = control(anvil) out = RUNS_DIR / "counts.json" - out.write_text(json.dumps({"repos": rows, "control": control()}, indent=1) + "\n") + out.write_text(json.dumps({"repos": rows, "control": ctl}, indent=1) + "\n") return out @@ -290,6 +241,8 @@ def report() -> Path: biggest = max(rows, key=lambda r: r["full_scan"]) worst_push = max(r["per_push_max"] for r in rows) ctl = run_doc["control"] + selection = json.loads((RULES / "selection.json").read_text()) + manifest = json.loads((RULES / "MANIFEST.json").read_text()) doc = { "id": ID, "measured_at": results.today(), @@ -299,10 +252,13 @@ def report() -> Path: "command": "python -m anvil_eval.experiments.candidates report", "git": results.git_state(), "pins": { - "opengrep": "v1.26.0 (eval/tools/opengrep/MANIFEST.toml)", - "gitlab_sast_rules": {"commit": recall.GITLAB.commit, - "dirs": list(recall.GITLAB_RULE_DIRS)}, - "0xdea_semgrep_rules": recall.OXDEA.commit, + "measured_through": "anvil recall, built from the commit in git", + "selection_sha256": manifest["selection_sha256"], + "selection_decided": f"{selection['decided_by']}, {selection['decided_on']}", + "rule_files": len(manifest["rules"]), + "corpora": {c["name"]: {"commit": c["commit"], "directories": c["directories"]} + for c in selection["corpora"]}, + "tools": {t["name"]: t["version"] for t in selection["tools"]}, "gosec": recall.GOSEC, "bandit": recall.BANDIT, "repos": {r["repo"]: r["commit"] for r in rows}, }, @@ -312,11 +268,13 @@ def report() -> Path: "largest_push": worst_push, "bars": {"full_scan": FULL_BAR, "per_push": PUSH_BAR}, "within_bars": biggest["full_scan"] < FULL_BAR and worst_push < PUSH_BAR, + "previous": PREVIOUS, }, "negative_control": { - "description": "a planted file per language family must each yield a candidate, and " - "a clean file none", - "expected": "every planted file flagged; no candidate in the clean file", + "description": "a planted file per language family must each yield a candidate, the " + "same files under tests/ none, and a clean file none", + "expected": "every planted file flagged; nothing under tests/; no candidate in the " + "clean file", "observed": {k: v for k, v in ctl.items() if k != "passed"}, "passed": ctl["passed"], }, @@ -337,11 +295,11 @@ def main(argv: list[str] | None = None) -> int: elif a.cmd == "report": print(report()) else: - found = dedupe(scan(a.root)) - print(json.dumps({"candidates": len(found)}, indent=1)) + with tempfile.TemporaryDirectory() as tmp: + doc = Anvil(Path(tmp)).recall(a.root) + print(json.dumps({k: v for k, v in doc.items() if k != "candidates"}, indent=1)) return 0 if __name__ == "__main__": sys.exit(main()) - diff --git a/eval/src/anvil_eval/recall.py b/eval/src/anvil_eval/recall.py index f44e9ae..95b0b49 100644 --- a/eval/src/anvil_eval/recall.py +++ b/eval/src/anvil_eval/recall.py @@ -6,9 +6,10 @@ by git); corpora and repositories under ``~/.cache/anvil-eval/recall``, outside the repository. GitLab's sast-rules is not uniformly MIT: its LICENSE puts ``doc/`` under CC BY-SA 4.0 and keeps -third-party components under their own licences. Only the seven rule directories in -``GITLAB_RULE_DIRS`` are ever read, which is the directory allowlist the plan asks Phase 6 to -enforce when it vendors them. +third-party components under their own licences, and its ``c/`` rules turned out to be one of them +(each is headed "License: GPL 2.0", generated from flawfinder). The checkout here is the whole +corpus at its pin; what Lane B runs is the owner's selection in ``data/rules/selection.json``, +vendored by ``anvil_eval.vendor_rules`` and enforced by ``internal/recall`` and its tests. """ from __future__ import annotations @@ -60,7 +61,6 @@ def path(self) -> Path: licence="MIT Expat (allowlisted dirs)") OXDEA = GitPin("0xdea-semgrep-rules", "https://github.com/0xdea/semgrep-rules.git", "dae50da6e6e629750f1daec73a01ef9206a372ab", 1, licence="MIT") -GITLAB_RULE_DIRS = ("c", "csharp", "go", "java", "javascript", "python", "scala") PUSHES = 20 REPOS = ( diff --git a/eval/src/anvil_eval/vendor_rules.py b/eval/src/anvil_eval/vendor_rules.py new file mode 100644 index 0000000..1ad6a20 --- /dev/null +++ b/eval/src/anvil_eval/vendor_rules.py @@ -0,0 +1,328 @@ +"""Vendor Lane B's selected rules into data/rules (plan node recall). + +Reads ``data/rules/selection.json`` (the owner's selection of 2026-10-03), takes each corpus from +its pinned checkout under ``~/.cache/anvil-eval/recall/rules`` (``python -m anvil_eval.recall +acquire`` fetches them), and writes: + +* every selected rule file, byte for byte, under ``data/rules//``; +* each corpus's LICENSE body, byte for byte, beside its rules; +* the Apache-2.0 body the gosec-derived Go rules are licensed under, from the pinned gosec release + tarball (its ``LICENSE.txt``); +* ``data/rules/MANIFEST.json``: per rule file, the corpus, repository, commit, path, git blob SHA-1, + SHA-256, rule ids, CWEs, languages and licence, with the evidence the licence was read from. + +Licences are read from the bodies, never from metadata. A GitLab rule file states a licence in a +``# License:`` header, and that header outranks the repository LICENSE: the C rules say GPL 2.0 +under a repository LICENSE that says MIT. But GitLab ported most of its rules from other +analysers, and the header of the rule's own companion test file (same name, the language's +extension) names where it came from: "LGPL-3.0 License (c) find-sec-bugs" beside a rule headed +"MIT (c) GitLab Inc." (the same-family review of 2026-10-03 found this; GitLab's mappings/ files +agree). So a GitLab rule's licence is the stricter of the two headers: a third-party companion +wins. The selection's ``exclude_by_derived_licence`` names the licences whose rules are left out +(LGPL-3.0, the owner's decision of 2026-10-03), and the manifest lists every rule it left out. A +rule file with no header takes its corpus's LICENSE body only when ``NO_PER_FILE_HEADERS`` names +its corpus. Any other licence outside MIT and Apache-2.0, or a header this module cannot read, +refuses the whole run: nothing is written. + +Only rule YAML is copied. The corpora's rule tests are third-party Go, Java and C sources, which +gate 3 (internal/dast/authz/egress_chokepoint_test.go) and the licence boundary both keep out of +this repository. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import shutil +import sys +import tarfile +from dataclasses import dataclass +from pathlib import Path + +import yaml + +from anvil_eval import REPO_ROOT, recall + +RULES_DIR = REPO_ROOT / "data" / "rules" +SELECTION = RULES_DIR / "selection.json" +MANIFEST = RULES_DIR / "MANIFEST.json" +GOSEC_TARBALL = recall.RECALL_BIN.parent / "gosec_2.29.0_linux_amd64.tar.gz" +GOSEC_LICENCE_NAME = "LICENSE.gosec-Apache-2.0.txt" +BANDIT_LICENCE = (recall.RECALL_VENV / "lib" / "python3.13" / "site-packages" + / "bandit-1.9.4.dist-info" / "licenses" / "LICENSE") +BANDIT_LICENCE_NAME = "LICENSE.bandit-Apache-2.0.txt" + +#: The licences a vendored rule may carry. Both are permissive and Apache-2.0 compatible. +ALLOWED = ("MIT", "Apache-2.0") + +#: A GitLab ``# License:`` header, as written in the pinned corpus, mapped to its SPDX id. Every +#: header seen at commit 53bf5cf6 is here; anything else is refused rather than guessed. +HEADERS = { + "MIT (c) GitLab Inc.": "MIT", + "License: MIT (c) GitLab Inc.": "MIT", # go/crypto/rule-tlsversion.yml repeats the word + "Apache 2.0 (c) gosec": "Apache-2.0", + "GPL 2.0 (c) 1989, 1991 Free Software Foundation, Inc.": "GPL-2.0", +} + +#: Corpora whose rule files carry no per-file licence header, so the corpus LICENSE body governs. +#: 0xdea's LICENSE is MIT, (c) 2022 raptor, and none of its rule files has a header. +NO_PER_FILE_HEADERS = {"0xdea-semgrep-rules"} + +HEADER_RE = re.compile(r"^#\s*License:\s*(.+?)\s*$", re.M) + +#: A GitLab companion test file's ``License:`` header, mapped to (SPDX id, upstream). Every header +#: seen beside a vendored rule at commit 53bf5cf6 is here; anything else is refused. +COMPANION_HEADERS = { + "LGPL-3.0 License (c) find-sec-bugs": ("LGPL-3.0", "find-sec-bugs"), + "LGPL-3.0 License (c) security-code-scan": ("LGPL-3.0", "security-code-scan"), + "Apache 2.0 (c) PyCQA": ("Apache-2.0", "bandit"), + "Apache 2.0 (c) gosec": ("Apache-2.0", "gosec"), + "Apache 2.0": ("Apache-2.0", ""), + "MIT (c) JS Foundation and other contributors, https://js.foundation": ("MIT", "JS Foundation"), + "MIT (c) GitLab Inc.": ("MIT", "GitLab"), + "MIT Copyright (c) 2022-Present GitLab B.V.": ("MIT", "GitLab"), +} +COMPANION_RE = re.compile(r"License:\s*(.+?)\s*$", re.M) + + +def companion_licence(root: Path, rel: str) -> tuple[str, str, str] | None: + """(SPDX, upstream, evidence) from the rule's companion test files, or None if it has none.""" + base = rel.rsplit(".", 1)[0] + found = [] + silent = [] + for f in sorted(root.glob(base + ".*")): + if f.suffix in (".yml", ".yaml"): + continue + m = COMPANION_RE.search("\n".join(f.read_text("utf-8", "replace").splitlines()[:15])) + if not m: + # No header gives no evidence either way; the rule's own header governs, and the + # evidence says the companion was silent. + silent.append(f.name) + continue + if m.group(1) not in COMPANION_HEADERS: + raise VendorError(f"{rel}: companion {f.name} has an unread licence header " + f"{m.group(1)!r}") + spdx, upstream = COMPANION_HEADERS[m.group(1)] + found.append((spdx, upstream, f"its GitLab companion {f.name}: License: {m.group(1)}")) + if not found: + if silent: + return ("", "", "its GitLab companion " + ", ".join(silent) + " states no licence") + return None + if len({f[0] for f in found}) != 1: + raise VendorError(f"{rel}: companions disagree on the licence: {found}") + return found[0] + + +class VendorError(RuntimeError): + """The selection cannot be vendored as stated; nothing was written.""" + + +@dataclass(frozen=True) +class Rule: + corpus: str + source_path: str + data: bytes + ids: list[str] + cwes: list[str] + languages: list[str] + licence: str + licence_evidence: str + + +def blob_sha1(data: bytes) -> str: + """Git's blob id: sha1 of ``blob \\0`` and the content.""" + return hashlib.sha1(b"blob %d\x00" % len(data) + data).hexdigest() + + +def sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _cwe(raw: str) -> str: + m = re.search(r"(?i)cwe-?(\d+)", raw) + if not m: + raise VendorError(f"unreadable CWE {raw!r}") + return f"CWE-{int(m.group(1))}" + + +def licence_of(corpus: str, path: str, text: str) -> tuple[str, str]: + headers = HEADER_RE.findall(text.split("\nrules:", 1)[0]) + if not headers: + if corpus in NO_PER_FILE_HEADERS: + return "", "" # plan() reads the corpus LICENSE body and states it + raise VendorError(f"{corpus}/{path} has no licence header") + spdx = {HEADERS.get(h) for h in headers} + if None in spdx or len(spdx) != 1: + raise VendorError(f"{corpus}/{path}: unreadable or conflicting licence headers {headers}") + return spdx.pop(), "# License: " + headers[0] + + +def read_rule(corpus: str, root: Path, rel: str) -> Rule: + data = (root / rel).read_bytes() + text = data.decode("utf-8") + licence, evidence = licence_of(corpus, rel, text) + doc = yaml.safe_load(text) + rules = (doc or {}).get("rules") or [] + if not rules: + raise VendorError(f"{corpus}/{rel} defines no rule") + ids, cwes, langs = [], [], [] + for r in rules: + ids.append(str(r["id"])) + raw = (r.get("metadata") or {}).get("cwe") + for c in raw if isinstance(raw, list) else [raw] if raw else []: + if _cwe(str(c)) not in cwes: + cwes.append(_cwe(str(c))) + for lang in r.get("languages") or []: + if lang not in langs: + langs.append(str(lang)) + if not cwes: + raise VendorError(f"{corpus}/{rel} names no CWE; every candidate needs one") + return Rule(corpus, rel, data, ids, cwes, langs, licence, evidence) + + +def derive(rule: Rule, root: Path) -> Rule: + """A GitLab rule's licence: the stricter of its own header and its companion's.""" + comp = companion_licence(root, rule.source_path) + if comp is None: + return Rule(**{**rule.__dict__, "licence_evidence": + rule.licence_evidence + "; it has no companion test file"}) + spdx, upstream, evidence = comp + if upstream in ("", "GitLab") or not spdx: + return Rule(**{**rule.__dict__, + "licence_evidence": rule.licence_evidence + "; " + evidence}) + return Rule(**{**rule.__dict__, "licence": spdx, "licence_evidence": + f"derived from {upstream}: {evidence}; the rule's own header says " + f"{rule.licence_evidence.removeprefix('# License: ')}"}) + + +def plan(selection: dict, corpus_root: Path) -> tuple[list[Rule], dict[str, bytes], list[dict]]: + """Every rule the selection admits, every licence body to archive, and every rule left out for + its derived licence. Writes nothing.""" + by_licence = selection.get("exclude_by_derived_licence") or {"licences": []} + left_out: list[dict] = [] + excluded_dirs = {(d["corpus"], d["directory"]) for d in selection["excluded_directories"]} + excluded_files = {(f["corpus"], f["path"]) for f in selection["excluded_rule_files"]} + rules: list[Rule] = [] + licences: dict[str, bytes] = {} + for c in selection["corpora"]: + root = corpus_root / c["name"] + head = (root / ".git" / "HEAD").read_text().strip() if (root / ".git").exists() else "" + if head != c["commit"]: + raise VendorError(f"{c['name']}: checkout HEAD is {head or 'absent'}, pinned " + f"{c['commit']}; run python -m anvil_eval.recall acquire") + for d in c["directories"]: + if (c["name"], d) in excluded_dirs: + raise VendorError(f"{c['name']}/{d} is both selected and excluded") + corpus_licence = (root / c["licence_file"]).read_bytes() + licences[f"{c['name']}/LICENSE"] = corpus_licence + for d in c["directories"]: + for f in sorted((root / d).rglob("*" + c["rule_suffix"])): + rel = f.relative_to(root).as_posix() + if (c["name"], rel) in excluded_files: + continue + rule = read_rule(c["name"], root, rel) + if c["name"] == "gitlab-sast-rules": + rule = derive(rule, root) + if rule.licence in by_licence["licences"]: + left_out.append({"path": f"{rule.corpus}/{rel}", "licence": rule.licence, + "evidence": rule.licence_evidence}) + continue + if not rule.licence: + body = corpus_licence.decode("utf-8").splitlines() + if not body or body[0].strip() != "MIT License": + raise VendorError(f"{c['name']}: cannot read its LICENSE body as MIT") + holder = next((ln.strip() for ln in body if ln.startswith("Copyright")), "") + rule = Rule(**{**rule.__dict__, "licence": "MIT", "licence_evidence": + f"the corpus LICENSE body: MIT License, {holder}; the rule " + f"file has no licence header of its own"}) + if rule.licence not in ALLOWED: + raise VendorError(f"{c['name']}/{rel} is {rule.licence}, which Lane B may not " + f"vendor ({rule.licence_evidence})") + rules.append(rule) + for f in selection["excluded_rule_files"]: + # An exclusion that names nothing would read as a decision that changed nothing. + if not (corpus_root / f["corpus"] / f["path"]).is_file(): + raise VendorError(f"excluded rule file {f['corpus']}/{f['path']} does not exist") + if any(r.licence_evidence.startswith("derived from bandit") for r in rules): + body = BANDIT_LICENCE.read_bytes() + if b"Apache License" not in body or b"Version 2.0" not in body: + raise VendorError("bandit's LICENSE is not the Apache-2.0 body") + licences[f"gitlab-sast-rules/{BANDIT_LICENCE_NAME}"] = body + if any(r.licence == "Apache-2.0" and not r.licence_evidence.startswith("derived from bandit") + for r in rules): + with tarfile.open(GOSEC_TARBALL) as t: + body = t.extractfile("LICENSE.txt").read() + if b"Apache License" not in body or b"Version 2.0" not in body: + raise VendorError("gosec's LICENSE.txt is not the Apache-2.0 body") + licences[f"gitlab-sast-rules/{GOSEC_LICENCE_NAME}"] = body + return rules, licences, left_out + + +def licence_file(r: Rule) -> str: + if r.licence == "Apache-2.0": + name = BANDIT_LICENCE_NAME if r.licence_evidence.startswith("derived from bandit") \ + else GOSEC_LICENCE_NAME + return f"gitlab-sast-rules/{name}" + return f"{r.corpus}/LICENSE" + + +def manifest(selection_bytes: bytes, selection: dict, rules: list[Rule], + licences: dict[str, bytes], left_out: list[dict] | None = None) -> dict: + by_corpus = {c["name"]: c for c in selection["corpora"]} + return { + "version": 1, + "generated_by": "python -m anvil_eval.vendor_rules", + "selection_sha256": sha256(selection_bytes), + "licences": [{"path": p, "sha256": sha256(b)} for p, b in sorted(licences.items())], + "rules": [{ + "corpus": r.corpus, + "repository": by_corpus[r.corpus]["repository"], + "commit": by_corpus[r.corpus]["commit"], + "path": f"{r.corpus}/{r.source_path}", + "blob_sha1": blob_sha1(r.data), + "sha256": sha256(r.data), + "ids": r.ids, + "cwes": r.cwes, + "languages": r.languages, + "licence": r.licence, + "licence_evidence": r.licence_evidence, + "licence_file": licence_file(r), + } for r in rules], + "excluded_by_licence": left_out or [], + } + + +def vendor(rules_dir: Path = RULES_DIR, corpus_root: Path | None = None) -> dict: + corpus_root = corpus_root or recall.RECALL_DATA / "rules" + selection_bytes = (rules_dir / "selection.json").read_bytes() + selection = json.loads(selection_bytes) + rules, licences, left_out = plan(selection, corpus_root) + doc = manifest(selection_bytes, selection, rules, licences, left_out) + for c in selection["corpora"]: + shutil.rmtree(rules_dir / c["name"], ignore_errors=True) + for r in rules: + out = rules_dir / r.corpus / r.source_path + out.parent.mkdir(parents=True, exist_ok=True) + out.write_bytes(r.data) + for p, b in licences.items(): + (rules_dir / p).parent.mkdir(parents=True, exist_ok=True) + (rules_dir / p).write_bytes(b) + (rules_dir / "MANIFEST.json").write_text(json.dumps(doc, indent=1) + "\n") + return {"rules": len(rules), "licences": sorted(licences), "excluded_by_licence": len(left_out)} + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(prog="python -m anvil_eval.vendor_rules") + ap.parse_args(argv) + try: + print(json.dumps(vendor(), indent=1)) + except VendorError as e: + print(f"refused: {e}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/eval/tests/fixtures/recall/planted/BadCipher.java b/eval/tests/fixtures/recall/planted/BadCipher.java new file mode 100644 index 0000000..59bde46 --- /dev/null +++ b/eval/tests/fixtures/recall/planted/BadCipher.java @@ -0,0 +1,11 @@ +// Planted for the candidates-per-scan control: DES. Never compiled. Since 2026-10-03 the Java plant +// trips GitLab's java_crypto_rule-CipherDESInsecure, one of the Java rules left after the +// LGPL-3.0-derived ones (find-sec-bugs) were excluded; the command-injection rule it used to trip +// is one of those. +import javax.crypto.Cipher; + +public class BadCipher { + Cipher cipher() throws Exception { + return Cipher.getInstance("DES/CBC/PKCS5Padding"); + } +} diff --git a/eval/tests/fixtures/recall/planted/BadExec.java b/eval/tests/fixtures/recall/planted/BadExec.java deleted file mode 100644 index 61f2005..0000000 --- a/eval/tests/fixtures/recall/planted/BadExec.java +++ /dev/null @@ -1,9 +0,0 @@ -// Planted for the candidates-per-scan control: a command built from a caller's string. Never -// executed. GitLab's java_inject_rule-CommandInjection is a taint rule whose sources include a -// String parameter and whose sinks include Runtime.exec, so the source here is a String parameter. -public class BadExec { - public static void run(String command) throws Exception { - Runtime rt = Runtime.getRuntime(); - rt.exec(command); - } -} diff --git a/eval/tests/test_candidates.py b/eval/tests/test_candidates.py index 0c5f2ee..5eb9470 100644 --- a/eval/tests/test_candidates.py +++ b/eval/tests/test_candidates.py @@ -1,7 +1,8 @@ -"""The candidates-per-scan counter's bookkeeping: diffs, de-duplication and refusals. +"""The candidates-per-scan instrument's bookkeeping: diffs, the anvil recall boundary, refusals. -The tools themselves are not in CI. The planted-fixture control that runs them is part of every -real run and is recorded in its artifact; it was first seen failing on 2026-10-03, when the Java +The tools themselves are not in the eval CI job; Lane B's own tests replay their recorded reports +and CI's Lane B job runs them. The planted-fixture control that runs them is part of every real +run and is recorded in its artifact; it was first seen failing on 2026-10-03, when the Java fixture did not match GitLab's taint rule (a String[] is not one of its sources). """ @@ -11,7 +12,6 @@ import pytest -from anvil_eval import recall from anvil_eval.experiments import candidates as cps @@ -45,55 +45,30 @@ def test_a_deleted_file_has_no_changed_lines(tmp_path): assert cps.changed_lines(tmp_path, "HEAD") == {} -def test_zero_padded_and_bare_cwe_ids_are_one_candidate(): - a = cps.Candidate("opengrep", "os-system", "x.py", 9, cps._cwe("CWE-078: OS Command")) - b = cps.Candidate("bandit", "B605", "x.py", 9, cps._cwe({"id": 78})) - assert len(cps.dedupe([a, b])) == 1 - - -def test_two_tools_on_one_line_and_cwe_are_one_candidate(): - a = cps.Candidate("opengrep", "eval", "x.py", 4, "CWE-95") - b = cps.Candidate("bandit", "B307", "x.py", 4, "CWE-95") - c = cps.Candidate("bandit", "B101", "x.py", 4, "CWE-703") - assert len(cps.dedupe([a, b, c])) == 2 - - -@pytest.mark.parametrize( - "raw,cwe", - [("CWE-95: Eval Injection", "CWE-95"), (["CWE-078"], "CWE-78"), ({"id": "22"}, "CWE-22"), - (None, ""), ("", "")], -) -def test_cwe_labels_from_each_tool(raw, cwe): - assert cps._cwe(raw) == cwe - - def test_a_missing_tool_is_a_refusal_not_zero(tmp_path, monkeypatch): - monkeypatch.setattr(recall, "opengrep_bin", lambda: tmp_path / "absent") - (tmp_path / "x.py").write_text("eval(input())\n") + anvil = cps.Anvil.__new__(cps.Anvil) + anvil.bin, anvil.config = tmp_path / "anvil", tmp_path / "anvil.yml" + anvil.bin.write_text("#!/bin/sh\necho 'recall: opengrep is not available' >&2\nexit 4\n") + anvil.bin.chmod(0o755) with pytest.raises(cps.ToolMissing): - cps.scan(tmp_path, [tmp_path / "x.py"]) + anvil.recall(tmp_path) -def test_a_tree_with_nothing_scannable_is_zero_without_running_anything(tmp_path, monkeypatch): - monkeypatch.setattr(recall, "opengrep_bin", lambda: tmp_path / "absent") - (tmp_path / "README.txt").write_text("hello\n") - assert cps.scan(tmp_path, [tmp_path / "README.txt"]) == [] +def test_a_failed_run_is_an_error_not_zero(tmp_path): + anvil = cps.Anvil.__new__(cps.Anvil) + anvil.bin, anvil.config = tmp_path / "anvil", tmp_path / "anvil.yml" + anvil.bin.write_text("#!/bin/sh\nexit 5\n") + anvil.bin.chmod(0o755) + with pytest.raises(cps.ToolFailed): + anvil.recall(tmp_path) -def test_only_the_allowlisted_gitlab_directories_are_read(): - assert recall.GITLAB_RULE_DIRS == ("c", "csharp", "go", "java", "javascript", "python", - "scala") - assert "doc" not in recall.GITLAB_RULE_DIRS # CC BY-SA 4.0 under GitLab's LICENSE +def test_candidates_are_read_from_anvil_recall_json(): + doc = {"candidates": [{"tool": "gosec", "ruleIdVersioned": "gosec/G204@2.29.0", "path": "a.go", + "startLine": 3, "cwe": "CWE-78"}]} + assert cps.candidates_of(doc) == [cps.Candidate("gosec", "gosec/G204@2.29.0", "a.go", 3, + "CWE-78")] -def test_go_test_files_alone_never_reach_gosec(tmp_path, monkeypatch): - calls = [] - monkeypatch.setattr(cps, "run_opengrep", lambda root, targets, has_c: []) - monkeypatch.setattr(cps, "run_gosec", lambda root, pkgs: calls.append(pkgs) or []) - (tmp_path / "go.mod").write_text("module m\n") - (tmp_path / "x_test.go").write_text("package m\n") - cps.scan(tmp_path, [tmp_path / "x_test.go"]) - assert calls == [] - (tmp_path / "x.go").write_text("package m\n") - cps.scan(tmp_path, [tmp_path / "x.go", tmp_path / "x_test.go"]) - assert calls == [["./."]] +def test_the_previous_measurement_is_carried_forward(): + assert cps.PREVIOUS["value"] == 10714 and cps.PREVIOUS["full_scan"]["curl"] == 10714 diff --git a/eval/tests/test_vendor_rules.py b/eval/tests/test_vendor_rules.py new file mode 100644 index 0000000..7df735e --- /dev/null +++ b/eval/tests/test_vendor_rules.py @@ -0,0 +1,179 @@ +"""The rule vendoring: licences read from rule headers, refusals before anything is written.""" + +from __future__ import annotations + +import io +import json +import tarfile + +import pytest + +from anvil_eval import vendor_rules as vr + +PIN = "a" * 40 +MIT = "MIT License\n\nCopyright (c) 2022 raptor\n\nPermission is hereby granted...\n" + + +GITLAB_MIT = "# License: MIT (c) GitLab Inc.\n" + + +APACHE = b"Apache License\nVersion 2.0, January 2004\n" + + +@pytest.fixture(autouse=True) +def licence_bodies(tmp_path, monkeypatch): + """The gosec release archive and bandit's licence are local acquisitions, absent in CI; each + test gets stand-ins holding an Apache-2.0 opening, which is all plan() checks.""" + tarball = tmp_path / "gosec.tar.gz" + with tarfile.open(tarball, "w:gz") as t: + info = tarfile.TarInfo("LICENSE.txt") + info.size = len(APACHE) + t.addfile(info, io.BytesIO(APACHE)) + bandit = tmp_path / "bandit-LICENSE" + bandit.write_bytes(APACHE) + monkeypatch.setattr(vr, "GOSEC_TARBALL", tarball) + monkeypatch.setattr(vr, "BANDIT_LICENCE", bandit) + + +def rule(header: str, rid: str = "r-1", cwe: str = "CWE-78") -> str: + return (f"{header}rules:\n - id: {rid}\n languages: [python]\n metadata:\n" + f" cwe: \"{cwe}\"\n") + + +def corpus(tmp_path, name, files): + root = tmp_path / "corpora" / name + (root / ".git").mkdir(parents=True) + (root / ".git" / "HEAD").write_text(PIN + "\n") + (root / "LICENSE").write_text(MIT) + for rel, body in files.items(): + (root / rel).parent.mkdir(parents=True, exist_ok=True) + (root / rel).write_text(body) + return tmp_path / "corpora" + + +def selection(name="gitlab-sast-rules", dirs=("python",), suffix=".yml", excluded_files=()): + return { + "version": 1, "decided_by": "t", "decided_on": "2026-10-03", "tools": [], + "corpora": [{"name": name, "repository": "https://example.invalid", "commit": PIN, + "licence_file": "LICENSE", "rule_suffix": suffix, "directories": list(dirs)}], + "excluded_directories": [], "excluded_rule_files": list(excluded_files), + "excluded_tool_rules": [], "excluded_paths": {"directories": [], "file_globs": []}, + } + + +def test_a_gitlab_header_decides_the_licence(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", { + "python/a.yml": rule("# License: MIT (c) GitLab Inc.\n"), + "python/b.yml": rule("# License: Apache 2.0 (c) gosec\n", "r-2"), + }) + rules, _, _ = vr.plan(selection(), root) + assert {r.licence for r in rules} == {"MIT", "Apache-2.0"} + + +def test_a_gpl_rule_refuses_the_whole_run(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", { + "python/a.yml": rule("# License: MIT (c) GitLab Inc.\n"), + "python/b.yml": rule("# License: GPL 2.0 (c) 1989, 1991 Free Software Foundation, Inc.\n", + "r-2"), + }) + with pytest.raises(vr.VendorError, match="GPL-2.0"): + vr.plan(selection(), root) + + +@pytest.mark.parametrize("header", ["", "# License: BSD-ish\n", "# License: MIT (c) GitLab Inc.\n" + "# License: Apache 2.0 (c) gosec\n"]) +def test_an_unread_or_conflicting_header_refuses(tmp_path, header): + root = corpus(tmp_path, "gitlab-sast-rules", {"python/a.yml": rule(header)}) + with pytest.raises(vr.VendorError): + vr.plan(selection(), root) + + +def test_a_corpus_without_headers_takes_its_licence_body(tmp_path): + root = corpus(tmp_path, "0xdea-semgrep-rules", {"rules/c/a.yaml": rule("")}) + rules, _, _ = vr.plan(selection("0xdea-semgrep-rules", ("rules/c",), ".yaml"), root) + assert rules[0].licence == "MIT" and "Copyright (c) 2022 raptor" in rules[0].licence_evidence + + +def test_a_checkout_off_its_pin_refuses(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", {"python/a.yml": rule(GITLAB_MIT)}) + (root / "gitlab-sast-rules" / ".git" / "HEAD").write_text("b" * 40 + "\n") + with pytest.raises(vr.VendorError, match="pinned"): + vr.plan(selection(), root) + + +def test_an_exclusion_must_name_a_real_file(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", {"python/a.yml": rule(GITLAB_MIT)}) + sel = selection(excluded_files=[{"corpus": "gitlab-sast-rules", "path": "python/gone.yml", + "reason": "x"}]) + with pytest.raises(vr.VendorError, match="does not exist"): + vr.plan(sel, root) + + +def test_a_refused_run_writes_nothing(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", { + "python/a.yml": rule("# License: GPL 2.0 (c) 1989, 1991 Free Software Foundation, Inc.\n")}) + out = tmp_path / "rules" + out.mkdir() + (out / "selection.json").write_text(json.dumps(selection())) + with pytest.raises(vr.VendorError): + vr.vendor(out, root) + assert sorted(p.name for p in out.iterdir()) == ["selection.json"] + + +def test_the_committed_manifest_matches_the_committed_selection(): + sel = vr.SELECTION.read_bytes() + man = json.loads(vr.MANIFEST.read_text()) + assert man["selection_sha256"] == vr.sha256(sel) + assert all(r["licence"] in vr.ALLOWED for r in man["rules"]) + assert len(man["excluded_by_licence"]) == 131 + assert all(e["licence"] == "LGPL-3.0" for e in man["excluded_by_licence"]) + assert not any(r["path"].startswith("gitlab-sast-rules/c/") for r in man["rules"]) + + +def test_a_companion_decides_when_it_names_an_upstream(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", { + "python/a.yml": rule(GITLAB_MIT), + "python/a.py": "# License: Apache 2.0 (c) PyCQA\nimport os\n", + "python/b.yml": rule(GITLAB_MIT, "r-2"), + "python/b.py": "# License: MIT (c) GitLab Inc.\n", + "python/c.yml": rule(GITLAB_MIT, "r-3"), + "python/c.py": "import os # no header\n", + }) + rules, _, left_out = vr.plan(selection(), root) + by = {r.source_path: r for r in rules} + assert by["python/a.yml"].licence == "Apache-2.0" + assert by["python/a.yml"].licence_evidence.startswith("derived from bandit") + assert by["python/b.yml"].licence == "MIT" + assert "states no licence" in by["python/c.yml"].licence_evidence + assert left_out == [] + + +def test_an_lgpl_companion_is_left_out_when_the_selection_excludes_it(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", { + "python/a.yml": rule(GITLAB_MIT), + "python/a.py": "// License: LGPL-3.0 License (c) find-sec-bugs\n", + "python/b.yml": rule(GITLAB_MIT, "r-2"), + }) + sel = selection() + sel["exclude_by_derived_licence"] = {"licences": ["LGPL-3.0"]} + rules, _, left_out = vr.plan(sel, root) + assert [r.source_path for r in rules] == ["python/b.yml"] + assert left_out[0]["licence"] == "LGPL-3.0" and "find-sec-bugs" in left_out[0]["evidence"] + + +def test_an_lgpl_companion_refuses_the_run_when_nothing_excludes_it(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", { + "python/a.yml": rule(GITLAB_MIT), + "python/a.py": "// License: LGPL-3.0 License (c) find-sec-bugs\n", + }) + with pytest.raises(vr.VendorError, match="LGPL-3.0"): + vr.plan(selection(), root) + + +def test_an_unread_companion_header_refuses(tmp_path): + root = corpus(tmp_path, "gitlab-sast-rules", { + "python/a.yml": rule(GITLAB_MIT), + "python/a.py": "# License: WTFPL\n", + }) + with pytest.raises(vr.VendorError, match="unread licence header"): + vr.plan(selection(), root) diff --git a/eval/tools/opengrep/README.md b/eval/tools/opengrep/README.md index 6a5ce17..380a938 100644 --- a/eval/tools/opengrep/README.md +++ b/eval/tools/opengrep/README.md @@ -1,5 +1,10 @@ # `eval/tools/opengrep` — the deterministic recall tier, pinned +> **Since Phase 6 (2026-10-03)** the engine pinned here (v1.26.0, by SHA-256) is the one Lane B runs, but the +> rule corpus is not: Lane B runs the owner's selection in `data/rules` (GitLab's sast-rules and 0xdea's C rules), +> through `internal/recall`. The Aikido corpus below, two GitHub-workflow rules, is kept only as the acquisition's +> record. + Anvil's **opengrep acquisition** (`plan/design/evaluation.md`). Acquires and invokes the recall-tier stand-in that the candidates-per-scan instrument (the spine's affordability rule) measures against: diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 9c8579b..d200376 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -17,6 +17,7 @@ import ( "github.com/Susquehanna-Syntax/Anvil/internal/collector/repo" "github.com/Susquehanna-Syntax/Anvil/internal/ingest/config" "github.com/Susquehanna-Syntax/Anvil/internal/ingest/delta" + "github.com/Susquehanna-Syntax/Anvil/internal/laneb" "github.com/Susquehanna-Syntax/Anvil/internal/scan" ) @@ -63,6 +64,7 @@ type Config struct { Policy string // the trigger policy for host requests TrivyDB scan.TrivyDB Trivy repo.Config + LaneB *laneb.Config // nil: Lane B is off in the configuration Version string Tick time.Duration Log io.Writer @@ -174,7 +176,7 @@ func handle(ctx context.Context, cfg Config, path string) Answer { Kind: req.Kind, RepoPath: req.Repo, TargetName: req.Target, Event: req.Event, Full: req.Full, PolicyPath: req.Policy, Cache: cfg.Cache, Store: cfg.Store, Feeds: cfg.Feeds, - TrivyDB: cfg.TrivyDB, Trivy: cfg.Trivy, AnvilVersion: cfg.Version, Now: cfg.Now, + TrivyDB: cfg.TrivyDB, Trivy: cfg.Trivy, LaneB: cfg.LaneB, AnvilVersion: cfg.Version, Now: cfg.Now, } if req.Kind == scan.KindHost { if sr.PolicyPath == "" { diff --git a/internal/laneb/doc.go b/internal/laneb/doc.go new file mode 100644 index 0000000..b7d2958 --- /dev/null +++ b/internal/laneb/doc.go @@ -0,0 +1,7 @@ +// Package laneb is the Lane B pipeline (plan node pipeline): it runs the +// recall tier over a repository, places every candidate on the record as an +// unconfirmed SAST finding with its rule's provenance, and hands any +// checked-in API specification to the dynamic tier's inventory through the +// SAST run's spec harvest. It owns the one mapping from a recall candidate to +// anvil/verdict, and that mapping has exactly one answer: unconfirmed. +package laneb diff --git a/internal/laneb/emit.go b/internal/laneb/emit.go new file mode 100644 index 0000000..7cdf4bd --- /dev/null +++ b/internal/laneb/emit.go @@ -0,0 +1,347 @@ +// This file places recall candidates on the record. +// +// THE VERDICT MAPPING IS OWNED HERE AND NOWHERE ELSE (CONTRACT.md §1.6). The +// owner's gate decision of 2026-10-03 deleted the adjudicator, so nothing in +// Lane B judges a match: every candidate becomes anvil/verdict unconfirmed, +// and the coding agent's triage gate decides it. VerdictFor is that mapping, +// and TestEveryCandidateReachesTheRecordUnconfirmed holds it. +// +// THE REST FOLLOWS FROM THE SAME DECISION. anvil/confidence is 1, as Lane A +// writes for its comparator: the pattern is certainly present, and whether it +// is a defect is the verdict's job. anvil/detector is kind sast with an empty +// model and revision, because no model ran. A ranker would write result.rank +// and never confidence; none ships in v1 (plan node encoder), so rank is unset. +// +// WHO WROTE WHICH BYTES (CONTRACT.md §2). The snippet, the context, the path, +// the symbol and the rule id all come from outside Anvil (the repository under +// scan, or a rule corpus), so the result's trust default is untrusted. The one +// string Anvil writes is anvil/reasoning, composed from the closed vocabulary +// below and base-10 integers this process computed, never from the bytes it +// describes; it alone is labelled anvil_generated. + +package laneb + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/Susquehanna-Syntax/Anvil/internal/recall" + "github.com/Susquehanna-Syntax/Anvil/internal/record" +) + +// VerdictFor is the Lane B pipeline's mapping from a recall candidate to +// anvil/verdict. A candidate is a rule match nobody has judged, so the answer +// does not depend on the candidate. +func VerdictFor(recall.Candidate) record.Verdict { return record.VerdictUnconfirmed } + +// The closed vocabulary anvil/reasoning is composed from. +const ( + reasonMatched = "A deterministic recall rule matched this location. No model judged the match: " + + "the finding is unconfirmed until the coding agent's triage gate decides it." + reasonCWE = "The rule's CWE, by number:" + reasonLines = "The match spans lines, first then last:" + reasonSymbol = "The enclosing function or class was named, by a parser or, for Python, by reading its indentation, so the finding's identity follows it rather than the line." + reasonNoSymbol = "No enclosing function was named, so the finding's identity follows the file." +) + +var reasoningVocabulary = map[string]bool{ + reasonMatched: true, reasonCWE: true, reasonLines: true, reasonSymbol: true, reasonNoSymbol: true, +} + +// errReasoning is a reasoning part outside the vocabulary: a bug, never data. +var errReasoning = errors.New("laneb: a reasoning part is neither vocabulary nor an integer this process computed") + +func composeReasoning(parts ...string) (string, error) { + for _, p := range parts { + if !reasoningVocabulary[p] && !isDigits(p) { + return "", fmt.Errorf("%w: %q", errReasoning, p) + } + } + return strings.Join(parts, " "), nil +} + +func isDigits(s string) bool { + if s == "" { + return false + } + for i := 0; i < len(s); i++ { + if s[i] < '0' || s[i] > '9' { + return false + } + } + return true +} + +// reasoningPointer is the RFC 6901 pointer of anvil/reasoning in a result. +const reasoningPointer = "/properties/anvil~1reasoning" + +// CWETaxonomyName names the CWE taxonomy in run.taxonomies. +const CWETaxonomyName = "CWE" + +// CWEVersion is the CWE catalogue Lane B's label space is (plan node +// candidatelist): CWE 4.20, the pinned archive under eval/data/cwe. +const CWEVersion = "4.20" + +// Emission is Lane B's contribution to the SAST run. +type Emission struct { + Results []record.Result + Extensions []record.ToolComponent + Taxonomies []record.ToolComponent + // Refused are candidates that could not be placed on the record, each + // with the reason. A refusal makes the scan incomplete. + Refused []string +} + +// Emit places candidates on the record for the target targetID. The order of +// the results is the candidates' order. +func Emit(targetID string, cands []recall.Candidate) (Emission, error) { + if strings.TrimSpace(targetID) == "" { + return Emission{}, errors.New("laneb: no target id; it is the first field of every anvil-fp/v1 digest") + } + var out Emission + + // Identity: anvil-fp/v1's SAST tier, with ordinals over identical matches. + type placed struct { + c recall.Candidate + cwe int + } + var keep []placed + var batch []record.SastCandidate + for _, c := range cands { + in := record.SastInput{ + TargetID: targetID, RuleIDVersioned: c.RuleIDVersioned, RepoRelPath: c.Path, + EnclosingSymbolPath: c.EnclosingSymbolPath(), Snippet: c.Snippet, + } + if _, err := record.SastFields(in); err != nil { + out.Refused = append(out.Refused, fmt.Sprintf("%s:%d %s: %v", c.Path, c.StartLine, c.RuleIDVersioned, err)) + continue + } + n, err := strconv.Atoi(strings.TrimPrefix(c.CWE, "CWE-")) + if err != nil || n <= 0 { + out.Refused = append(out.Refused, fmt.Sprintf("%s:%d %s: CWE %q is not CWE-", c.Path, c.StartLine, c.RuleIDVersioned, c.CWE)) + continue + } + keep = append(keep, placed{c: c, cwe: n}) + batch = append(batch, record.SastCandidate{Input: in, Line: c.StartLine}) + } + inputs, err := record.AssignSastOrdinals(batch) + if err != nil { + return Emission{}, err + } + + ext := newExtensions() + cwes := map[int]bool{} + lineSeen := map[string]int{} + for i, p := range keep { + c := p.c + digest, err := record.Sast(inputs[i]) + if err != nil { + return Emission{}, err + } + if err := ext.cite(c); err != nil { + return Emission{}, err + } + cwes[p.cwe] = true + + symbolReason := reasonNoSymbol + if c.Symbol != "" { + symbolReason = reasonSymbol + } + reasoning, err := composeReasoning(reasonMatched, reasonCWE, strconv.Itoa(p.cwe), + reasonLines, strconv.Itoa(c.StartLine), strconv.Itoa(c.EndLine), symbolReason) + if err != nil { + return Emission{}, err + } + + lh := lineHash(c) + lineSeen[c.Path+"\x00"+lh]++ + zero, idx := 0, 0 // idx is set below, once every source is known + res := record.Result{ + RuleID: c.RuleIDVersioned, + // SARIF's "open": the tool could not determine whether the + // result is a problem. It is the SARIF-native reading of + // unconfirmed. + Kind: record.KindOpen, + Message: record.Message{Text: message(c)}, + Rule: &record.ReportingDescriptorReference{ + ID: c.RuleIDVersioned, ToolComponent: &record.ToolComponentRef{Index: &idx}, + }, + Locations: []record.Location{location(c)}, + Taxa: []record.ReportingDescriptorReference{{ + ID: strconv.Itoa(p.cwe), ToolComponent: &record.ToolComponentRef{Name: CWETaxonomyName, Index: &zero}, + }}, + PartialFingerprints: map[string]string{ + record.PartialFingerprintAnvilFindingID: digest, + record.PartialFingerprintPrimaryLocationLineHash: lh + ":" + + strconv.Itoa(lineSeen[c.Path+"\x00"+lh]), + }, + Properties: record.ResultProperties{ + FindingID: digest, + Half: record.HalfSast, + Confidence: 1, + Verdict: VerdictFor(c), + // First-party source is the coding agent's write surface. An + // unconfirmed finding still waits for the triage gate before + // anything is generated (record.taskcard's routing). + RemediableByAgent: true, + Reasoning: reasoning, + Detector: record.DetectorRef{Kind: record.DetectorKindSast}, + EvidenceClass: record.EvidenceClassSastStaticOnly, + Trust: record.TrustAssertion{ + Default: record.TrustUntrusted, + Fields: map[string]record.Trust{reasoningPointer: record.TrustAnvilGenerated}, + }, + }, + } + if err := record.ValidateResultTrust(&res); err != nil { + return Emission{}, fmt.Errorf("laneb: a result failed the trust check: %w", err) + } + out.Results = append(out.Results, res) + } + out.Extensions = ext.components() + pos := map[string]int{} + for i, c := range out.Extensions { + pos[c.Name] = i + } + for i := range out.Results { + n := pos[keepSource(out.Results[i], out.Extensions)] + out.Results[i].Rule.ToolComponent.Index = &n + } + if len(cwes) > 0 { + out.Taxonomies = []record.ToolComponent{cweTaxonomy(cwes)} + } + return out, nil +} + +// lineHash is the primaryLocationLineHash Lane B writes: the first 16 hex +// characters of the SHA-256 of the match's first line with its whitespace +// collapsed, then ":" and that hash's 1-based occurrence in the file among this +// scan's results. It is stable when lines move and when indentation changes, +// which is what GitHub reads the key for. It is Anvil's own construction, not +// CodeQL's rolling hash; the key's owner is still an open question in the +// contract (CONTRACT.md, the GitHub projection), and this is the value the +// Lane B pipeline produces until it is answered. +func lineHash(c recall.Candidate) string { + first, _, _ := strings.Cut(c.Snippet, "\n") + sum := sha256.Sum256([]byte(strings.Join(strings.Fields(first), " "))) + return hex.EncodeToString(sum[:8]) +} + +// message is the result's message: which rule matched where. It quotes a rule +// id and a path, so it is untrusted text, covered by the result's default. +func message(c recall.Candidate) string { + where := c.Path + ":" + strconv.Itoa(c.StartLine) + if c.Symbol != "" { + where += " (in " + c.Symbol + ")" + } + return "Unconfirmed " + c.CWE + " candidate: rule " + c.RuleID + " from " + c.Provenance.Source + + " matched at " + where + ". Nothing has judged the match; the triage gate decides it." +} + +func location(c recall.Candidate) record.Location { + loc := record.Location{PhysicalLocation: &record.PhysicalLocation{ + ArtifactLocation: record.ArtifactLocation{URI: c.Path}, + Region: &record.Region{ + StartLine: c.StartLine, EndLine: c.EndLine, + Snippet: &record.Snippet{Text: c.Snippet}, + }, + ContextRegion: &record.Region{ + StartLine: c.ContextStartLine, EndLine: c.ContextEndLine, + Snippet: &record.Snippet{Text: c.Context}, + }, + }} + if c.Symbol != "" { + name := c.Symbol[strings.LastIndex(c.Symbol, ".")+1:] + // No kind: the innermost enclosing block may be a function or a + // class, and Lane B states only what a parser named. + loc.LogicalLocations = []record.LogicalLocation{{ + Name: name, FullyQualifiedName: c.EnclosingSymbolPath(), + }} + } + return loc +} + +// extensions collects one tool extension per rule source, each holding the +// rules this scan's results cite, in a deterministic order. +type extensions struct { + order []string + by map[string]*record.ToolComponent + rules map[string]map[string]bool +} + +func newExtensions() *extensions { + return &extensions{by: map[string]*record.ToolComponent{}, rules: map[string]map[string]bool{}} +} + +// keepSource names the extension that holds a result's rule. +func keepSource(r record.Result, comps []record.ToolComponent) string { + for _, c := range comps { + for _, d := range c.Rules { + if d.ID == r.RuleID { + return c.Name + } + } + } + return "" +} + +func (e *extensions) cite(c recall.Candidate) error { + p := c.Provenance + prov := &record.RuleProvenance{ + Source: p.Source, Repository: p.Repository, Version: p.Version, RulePath: p.RulePath, + LicenseSpdx: p.Licence, LicenseEvidence: p.LicenceEvidence, + } + if err := prov.Validate(); err != nil { + return fmt.Errorf("laneb: %s cites a rule with incomplete provenance: %w", c.RuleIDVersioned, err) + } + comp, ok := e.by[p.Source] + if !ok { + comp = &record.ToolComponent{Name: p.Source, Version: p.Version, InformationURI: p.Repository} + e.by[p.Source] = comp + e.rules[p.Source] = map[string]bool{} + e.order = append(e.order, p.Source) + } + if !e.rules[p.Source][c.RuleIDVersioned] { + e.rules[p.Source][c.RuleIDVersioned] = true + comp.Rules = append(comp.Rules, record.ReportingDescriptor{ + ID: c.RuleIDVersioned, Name: c.RuleID, + Properties: &record.RuleProperties{RuleProvenance: prov}, + }) + } + return nil +} + +// components returns the extensions sorted by name, so their order does not +// depend on which result came first. +func (e *extensions) components() []record.ToolComponent { + names := append([]string(nil), e.order...) + sort.Strings(names) + out := make([]record.ToolComponent, 0, len(names)) + for _, n := range names { + c := *e.by[n] + sort.Slice(c.Rules, func(i, j int) bool { return c.Rules[i].ID < c.Rules[j].ID }) + out = append(out, c) + } + return out +} + +func cweTaxonomy(cwes map[int]bool) record.ToolComponent { + ids := make([]int, 0, len(cwes)) + for n := range cwes { + ids = append(ids, n) + } + sort.Ints(ids) + t := record.ToolComponent{ + Name: CWETaxonomyName, Version: CWEVersion, Organization: "MITRE", + InformationURI: "https://cwe.mitre.org/data/index.html", + } + for _, n := range ids { + t.Taxa = append(t.Taxa, record.ReportingDescriptor{ID: strconv.Itoa(n)}) + } + return t +} diff --git a/internal/laneb/gate_test.go b/internal/laneb/gate_test.go new file mode 100644 index 0000000..c6b1b0e --- /dev/null +++ b/internal/laneb/gate_test.go @@ -0,0 +1,57 @@ +package laneb + +import ( + "encoding/json" + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// TestTheGateFileCitesWhatExists holds docs/gates/lane-b.json to its own +// rule: every row names evidence, every cited path exists, and every cited +// test is defined in the file the row names. +func TestTheGateFileCitesWhatExists(t *testing.T) { + root := filepath.Join("..", "..") + raw, err := os.ReadFile(filepath.Join(root, "docs", "gates", "lane-b.json")) + if err != nil { + t.Fatal(err) + } + var gate struct { + Rows []struct { + Criterion string `json:"criterion"` + Status string `json:"status"` + Evidence []struct { + Test string `json:"test"` + Path string `json:"path"` + } `json:"evidence"` + } `json:"rows"` + } + dec := json.NewDecoder(strings.NewReader(string(raw))) + if err := dec.Decode(&gate); err != nil { + t.Fatal(err) + } + if len(gate.Rows) == 0 { + t.Fatal("the gate file has no rows") + } + for _, row := range gate.Rows { + if row.Criterion == "" || row.Status == "" || len(row.Evidence) == 0 { + t.Errorf("row %q names no evidence or no status", row.Criterion) + } + for _, ev := range row.Evidence { + src, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(ev.Path))) + if err != nil { + t.Errorf("row %q cites %s: %v", row.Criterion, ev.Path, err) + continue + } + if ev.Test == "" { + continue + } + def := regexp.MustCompile(`(?m)^(func|def) ` + regexp.QuoteMeta(ev.Test) + `\(`) + if !def.Match(src) { + t.Errorf("row %q cites %s in %s, which does not define it", row.Criterion, ev.Test, ev.Path) + } + } + } +} diff --git a/internal/laneb/harvest.go b/internal/laneb/harvest.go new file mode 100644 index 0000000..be455fb --- /dev/null +++ b/internal/laneb/harvest.go @@ -0,0 +1,145 @@ +// This file is the SAST half's spec harvest: it finds the API specifications +// a repository checks in (OpenAPI, Swagger, AsyncAPI, GraphQL SDL, WSDL, +// Postman collections) and carries them on the SAST run, where the dynamic +// tier's repo spec reader picks them up. plan/design/dynamic-tier.md:628-630 +// makes harvesting the SAST tier's job and forbids the DAST tier from doing +// it, so this is the only place a checked-in spec enters Anvil. +// +// It CLAIMS NO FORMAT. declaredFormat is left empty: the repo spec reader +// classifies from the bytes, and its format vocabulary is the dynamic tier's +// growing allowlist, which the core binary does not import (TestSplit) and +// must not copy. +// +// IT ACCOUNTS FOR WHAT IT DROPS. A spec file it found and did not carry +// (larger than MaxSpecBytes, not valid UTF-8, or past MaxSpecFiles) is counted +// in omittedFileCount, never silently left out: a missing spec shrinks the +// DAST coverage denominator, and a smaller denominator reads as better +// coverage. + +package laneb + +import ( + "crypto/sha256" + "encoding/hex" + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + "unicode/utf8" + + "github.com/Susquehanna-Syntax/Anvil/internal/record" +) + +// MaxSpecBytes is the largest spec file carried, the repo spec reader's own +// per-file bound (4 MiB). +const MaxSpecBytes = 4 << 20 + +// MaxSpecFiles is the most spec files one harvest carries, the repo spec +// reader's per-ingest bound. +const MaxSpecFiles = 4096 + +// readAtMost reads a file, or returns nil when it holds more than n bytes, +// however large it grew after it was measured. +func readAtMost(path string, n int64) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + b, err := io.ReadAll(io.LimitReader(f, n+1)) + if err != nil { + return nil, err + } + if int64(len(b)) > n { + return nil, nil + } + return b, nil +} + +// IsSpecFile reports whether a file name is one the harvest carries. +func IsSpecFile(name string) bool { + n := strings.ToLower(name) + for _, base := range []string{"openapi", "swagger", "asyncapi"} { + for _, ext := range []string{".json", ".yaml", ".yml"} { + if n == base+ext || strings.HasSuffix(n, "."+base+ext) { + return true + } + } + } + for _, ext := range []string{".graphql", ".graphqls", ".gql", ".wsdl", ".postman_collection.json"} { + if strings.HasSuffix(n, ext) { + return true + } + } + return false +} + +// Harvest walks root and returns the SAST run's anvil/specHarvest. It never +// follows a symbolic link and never enters .git. An error walking the tree is +// returned, never reported as an empty harvest. +func Harvest(root string) (*record.SpecHarvest, error) { + abs, err := filepath.Abs(root) + if err != nil { + return nil, err + } + var found []string + err = filepath.WalkDir(abs, func(full string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + if d.Name() == ".git" && full != abs { + return filepath.SkipDir + } + return nil + } + if d.Type().IsRegular() && IsSpecFile(d.Name()) { + found = append(found, full) + } + return nil + }) + if err != nil { + return nil, err + } + sort.Strings(found) + h := &record.SpecHarvest{Outcome: record.SpecHarvestRan, Files: []record.SpecHarvestFile{}} + omitted := 0 + for _, full := range found { + if len(h.Files) >= MaxSpecFiles { + omitted++ + continue + } + st, err := os.Lstat(full) + if err != nil { + return nil, err + } + if st.Size() > MaxSpecBytes { + omitted++ + continue + } + b, err := readAtMost(full, MaxSpecBytes) + if err != nil { + return nil, err + } + if b == nil || !utf8.Valid(b) { // nil: it grew past the bound after Lstat + omitted++ + continue + } + rel, _ := filepath.Rel(abs, full) + sum := sha256.Sum256(b) + h.Files = append(h.Files, record.SpecHarvestFile{ + Location: record.ArtifactLocation{URI: filepath.ToSlash(rel)}, + SizeBytes: len(b), + ContentSHA256: hex.EncodeToString(sum[:]), + Content: &record.ArtifactContent{Text: string(b)}, + Trust: record.TrustUntrusted, + }) + } + h.OmittedFileCount = &omitted + if err := record.ValidateSpecHarvest(h); err != nil { + return nil, err + } + return h, nil +} diff --git a/internal/laneb/lane.go b/internal/laneb/lane.go new file mode 100644 index 0000000..11721dd --- /dev/null +++ b/internal/laneb/lane.go @@ -0,0 +1,141 @@ +// This file is the Lane B pipeline's two steps, as the scan controller's +// caller (internal/scan) uses them: Prepare before the audit begins, so a +// missing tool or a refused rule pack stops the scan with nothing recorded, +// then Run once the audit is open. + +package laneb + +import ( + "context" + "fmt" + "sort" + "strings" + "time" + + "github.com/Susquehanna-Syntax/Anvil/internal/recall" + "github.com/Susquehanna-Syntax/Anvil/internal/record" +) + +// FullScanBudget is the affordability bar for one full scan: under 500 +// candidates (the candidates-per-scan register row). A scan at or over it is +// flagged on every scan, because candidate volume moves with every rule +// update and every repository. +const FullScanBudget = 500 + +// Config is how an installation runs Lane B. +type Config struct { + // Rules is the rule pack directory (data/rules in a checkout). + Rules string + Tools recall.Tools + Exec recall.Exec +} + +// Lane is a prepared Lane B scan of one tree. +type Lane struct { + scanner recall.Scanner + plan recall.Plan +} + +// Prepare verifies the rule pack, enumerates root and resolves every tool +// the tree needs. Its errors wrap recall.ErrRulePack, recall.ErrToolAbsent or +// recall.ErrToolFailed (a tool at the wrong version). +func Prepare(ctx context.Context, cfg Config, root string) (*Lane, error) { + pack, err := recall.LoadPack(cfg.Rules) + if err != nil { + return nil, err + } + s := recall.Scanner{Pack: pack, Tools: cfg.Tools, Exec: cfg.Exec} + plan, err := s.Prepare(ctx, root) + if err != nil { + return nil, err + } + return &Lane{scanner: s, plan: plan}, nil +} + +// RulesetVersion names the rule selection, for scan_run.ruleset_version. +func (l *Lane) RulesetVersion() string { + return "laneb/selection@" + l.scanner.Pack.SelectionSHA256[:12] +} + +// EngineVersion names the tools this scan runs, at their pinned versions. +func (l *Lane) EngineVersion() string { + var parts []string + for _, t := range l.plan.Needs() { + pin, _ := l.scanner.Pack.Tool(t) + parts = append(parts, t+" "+pin.Version) + } + if len(parts) == 0 { + return "none (no source file in a covered language)" + } + return strings.Join(parts, ", ") +} + +// Snapshot is the SAST run's anvil/advisorySnapshot when no advisory feed was +// read: the rule pack is the corpus Lane B matched against, named by its +// selection digest and dated by the owner's decision that fixed it. +func (l *Lane) Snapshot() *record.AdvisorySnapshot { + at, err := time.Parse("2006-01-02", l.scanner.Pack.Selection.DecidedOn) + if err != nil { + at = time.Time{} + } + return &record.AdvisorySnapshot{ + FeedIDs: []string{"laneb-rule-pack"}, + SnapshotDigest: l.RulesetVersion(), + ScrapedAt: at.UTC(), + } +} + +// Output is one Lane B run. +type Output struct { + Emission + Recall recall.Result + SpecHarvest *record.SpecHarvest + // Problems make the scan incomplete; Notes do not. + Problems []string + Notes []string +} + +// Run scans the prepared tree for the target targetID and places every +// candidate on the record. +func (l *Lane) Run(ctx context.Context, targetID string) (Output, error) { + var out Output + res, err := l.scanner.Scan(ctx, l.plan) + if err != nil { + return out, err + } + out.Recall = res + out.Problems = append(out.Problems, res.Problems...) + out.Notes = append(out.Notes, res.Notes...) + if l.plan.Empty() { + out.Problems = append(out.Problems, fmt.Sprintf( + "Lane B found no source file in a language its rules cover (%s), so it scanned nothing; that is not a clean repository", + strings.Join(l.scanner.Pack.Languages(), ", "))) + } + em, err := Emit(targetID, res.Candidates) + if err != nil { + return out, err + } + out.Emission = em + for _, r := range em.Refused { + out.Problems = append(out.Problems, "a candidate could not be placed on the record: "+r) + } + if out.SpecHarvest, err = Harvest(l.plan.Root); err != nil { + return out, fmt.Errorf("laneb: the spec harvest failed: %w", err) + } + if res.Count >= FullScanBudget { + out.Notes = append(out.Notes, fmt.Sprintf( + "Lane B produced %d candidates, at or over the budget of %d a full scan: the recall tier needs re-scoping for this repository (eval/register.yaml, candidates-per-scan)", + res.Count, FullScanBudget)) + } + if res.PartialParses > 0 { + out.Notes = append(out.Notes, fmt.Sprintf("opengrep parsed %d file(s) only in part", res.PartialParses)) + } + tools := make([]string, 0, len(res.ByTool)) + for t, n := range res.ByTool { + tools = append(tools, fmt.Sprintf("%s %d", t, n)) + } + sort.Strings(tools) + out.Notes = append(out.Notes, fmt.Sprintf("Lane B: %d candidate(s) from %d file(s) (%s); %d excluded by the selection's paths", + res.Count, res.Files, strings.Join(tools, ", "), l.plan.Excluded)) + return out, nil +} diff --git a/internal/laneb/laneb_test.go b/internal/laneb/laneb_test.go new file mode 100644 index 0000000..e16e29d --- /dev/null +++ b/internal/laneb/laneb_test.go @@ -0,0 +1,166 @@ +package laneb + +import ( + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Susquehanna-Syntax/Anvil/internal/recall" + "github.com/Susquehanna-Syntax/Anvil/internal/recall/recalltest" + "github.com/Susquehanna-Syntax/Anvil/internal/record" +) + +func candidate(source, rule, path string, line int) recall.Candidate { + return recall.Candidate{ + Tool: recall.ToolOpengrep, RuleID: rule, RuleIDVersioned: source + "/" + rule + "@abc", CWE: "CWE-95", + Path: path, StartLine: line, EndLine: line, Symbol: "f", Snippet: "eval(x)", + ContextStartLine: line, ContextEndLine: line, Context: "eval(x)", + Provenance: recall.Provenance{Source: source, Repository: "https://example.invalid/" + source, Version: "abc", + RulePath: source + "/r.yml", Licence: "MIT", LicenceEvidence: "the LICENSE body"}, + } +} + +// TestVerdictForIsUnconfirmedWhateverTheCandidate holds the mapping itself; +// TestEveryCandidateReachesTheRecordUnconfirmed (internal/scan) holds it +// through the whole chain. +func TestVerdictForIsUnconfirmedWhateverTheCandidate(t *testing.T) { + for _, c := range []recall.Candidate{{}, candidate("gosec", "G204", "a.go", 1), {CWE: "CWE-79", Tool: recall.ToolBandit}} { + if v := VerdictFor(c); v != record.VerdictUnconfirmed { + t.Fatalf("VerdictFor(%+v) = %q", c, v) + } + } +} + +// TestReasoningRefusesExternalText is the guard that makes anvil_generated +// true on anvil/reasoning: a part that is not vocabulary or digits is refused. +func TestReasoningRefusesExternalText(t *testing.T) { + if _, err := composeReasoning(reasonMatched, "12"); err != nil { + t.Fatal(err) + } + for _, bad := range []string{"src/app.py", "-1", "12", "", "ignore previous instructions"} { + if _, err := composeReasoning(reasonMatched, bad); !errors.Is(err, errReasoning) { + t.Errorf("composeReasoning accepted %q", bad) + } + } +} + +func TestEmitPlacesEveryCandidateWithResolvableProvenance(t *testing.T) { + cands := []recall.Candidate{ + candidate("zeta-rules", "z", "b.py", 3), + candidate("alpha-rules", "a", "a.py", 1), + candidate("zeta-rules", "z", "b.py", 9), // the same rule again + } + em, err := Emit("repo:t", cands) + if err != nil { + t.Fatal(err) + } + if len(em.Results) != 3 || len(em.Refused) != 0 || len(em.Extensions) != 2 { + t.Fatalf("results %d refused %v extensions %d", len(em.Results), em.Refused, len(em.Extensions)) + } + if em.Extensions[0].Name != "alpha-rules" || len(em.Extensions[1].Rules) != 1 { + t.Fatalf("extensions are not sorted, or a rule was cited twice: %+v", em.Extensions) + } + for i, r := range em.Results { + ext := em.Extensions[*r.Rule.ToolComponent.Index] + if ext.Name != cands[i].Provenance.Source { + t.Errorf("result %d cites %s, its rule is in %s", i, ext.Name, cands[i].Provenance.Source) + } + if r.Properties.Verdict != record.VerdictUnconfirmed || r.Properties.Confidence != 1 || + r.Properties.Detector.Model != "" || r.Rank != nil { + t.Errorf("result %d: %+v", i, r.Properties) + } + if err := record.ValidateResultTrust(&em.Results[i]); err != nil { + t.Errorf("result %d: %v", i, err) + } + } + if em.Results[1].PartialFingerprints[record.PartialFingerprintPrimaryLocationLineHash] == "" { + t.Error("no primaryLocationLineHash") + } +} + +func TestEmitRefusesIncompleteProvenance(t *testing.T) { + c := candidate("x", "r", "a.py", 1) + c.Provenance.Licence = "" + if _, err := Emit("repo:t", []recall.Candidate{c}); err == nil || !strings.Contains(err.Error(), "provenance") { + t.Fatalf("a candidate with no licence was placed on the record: %v", err) + } +} + +func TestEmitRefusesACandidateItCannotIdentify(t *testing.T) { + c := candidate("x", "r", "a.py", 1) + c.Snippet = " # only a comment" + em, err := Emit("repo:t", []recall.Candidate{c}) + if err != nil || len(em.Results) != 0 || len(em.Refused) != 1 { + t.Fatalf("a snippet with no identity: results %d refused %v err %v", len(em.Results), em.Refused, err) + } +} + +func TestHarvest(t *testing.T) { + root := t.TempDir() + write := func(rel string, b []byte) { + p := filepath.Join(root, rel) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, b, 0o644); err != nil { + t.Fatal(err) + } + } + write("docs/openapi.yaml", []byte("openapi: 3.0.3\n")) + write("schema.graphql", []byte("type Query { a: Int }\n")) + write("api/big.swagger.json", make([]byte, MaxSpecBytes+1)) + write("api/bin.wsdl", []byte{0xff, 0xfe}) + write("README.md", []byte("not a spec")) + write(".git/openapi.json", []byte("{}")) + outside := t.TempDir() + if err := os.WriteFile(filepath.Join(outside, "openapi.json"), []byte("{}"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join(outside, "openapi.json"), filepath.Join(root, "linked.openapi.json")); err != nil { + t.Fatal(err) + } + h, err := Harvest(root) + if err != nil { + t.Fatal(err) + } + var got []string + for _, f := range h.Files { + got = append(got, f.Location.URI) + if f.Trust != record.TrustUntrusted || f.DeclaredFormat != "" || f.Content == nil || len(f.Content.Text) != f.SizeBytes { + t.Errorf("%s: %+v", f.Location.URI, f) + } + } + if strings.Join(got, ",") != "docs/openapi.yaml,schema.graphql" { + t.Errorf("harvested %v", got) + } + if h.OmittedFileCount == nil || *h.OmittedFileCount != 2 { + t.Errorf("omitted %v, want 2 (one over size, one not UTF-8)", h.OmittedFileCount) + } +} + +// TestTheFixtureThroughTheLane runs Prepare and Run over the fixture with the +// tools' recorded reports. +func TestTheFixtureThroughTheLane(t *testing.T) { + root, _ := filepath.Abs(recalltest.FixtureRoot()) + cfg := Config{Rules: recalltest.RulePack(), Tools: recalltest.Tools(), Exec: &recalltest.Replay{Root: root}} + lane, err := Prepare(context.Background(), cfg, root) + if err != nil { + t.Fatal(err) + } + out, err := lane.Run(context.Background(), "repo:laneb-fixture") + if err != nil { + t.Fatal(err) + } + if out.Recall.Count != 12 || len(out.Results) != 12 || len(out.Problems) != 0 { + t.Fatalf("count %d results %d problems %v", out.Recall.Count, len(out.Results), out.Problems) + } + if out.SpecHarvest == nil || len(out.SpecHarvest.Files) != 1 || out.SpecHarvest.Files[0].Location.URI != "api/openapi.yaml" { + t.Fatalf("spec harvest %+v", out.SpecHarvest) + } + if !strings.HasPrefix(lane.RulesetVersion(), "laneb/selection@") || lane.Snapshot().ScrapedAt.IsZero() { + t.Errorf("ruleset %q snapshot %+v", lane.RulesetVersion(), lane.Snapshot()) + } +} diff --git a/internal/recall/candidate.go b/internal/recall/candidate.go new file mode 100644 index 0000000..12f0171 --- /dev/null +++ b/internal/recall/candidate.go @@ -0,0 +1,642 @@ +// This file is the candidate contract (plan node candidatelist) and the scan +// that produces candidates. +// +// A CANDIDATE IS A RULE MATCH NOBODY HAS JUDGED. It carries: +// +// - a function-authoritative identity: the enclosing symbol, named by a +// parser (go/parser for any Go match, opengrep's own for its other +// matches, an indentation reading for bandit's), or empty when the match +// is top-level and the file alone identifies it; +// - an advisory-only line hint: lines move with every edit above them, so +// they locate a match for a reader and never identify it; +// - the rule id, versioned, and one CWE; +// - the snippet, verbatim target-repository source and therefore untrusted +// whatever Anvil did to extract it; +// - the rule's provenance: repository, commit or release, licence. +// +// THE COUNT IS PART OF THE CONTRACT. Candidates per scan, not model size, +// decides whether Lane B is affordable (the candidates-per-scan register row), +// so every Scan returns it and the scan controller records it on every scan. + +package recall + +import ( + "bufio" + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" +) + +// Provenance is where a candidate's rule came from. Every field is non-empty +// on every candidate; Scan refuses to return one that is not. +type Provenance struct { + // Source is the corpus name ("gitlab-sast-rules") or the native tool + // ("gosec"). + Source string `json:"source"` + // Repository is the upstream repository URL. + Repository string `json:"repository"` + // Version is the corpus commit or the tool release. + Version string `json:"version"` + // RulePath is the vendored rule file, relative to the pack root, or the + // tool's own rule id for a native analyser. + RulePath string `json:"rulePath"` + // Licence is the rule's SPDX licence, and LicenceEvidence what it was read + // from. + Licence string `json:"licence"` + LicenceEvidence string `json:"licenceEvidence"` +} + +// Candidate is one recall candidate. +type Candidate struct { + Tool string `json:"tool"` + RuleID string `json:"ruleId"` + RuleIDVersioned string `json:"ruleIdVersioned"` + CWE string `json:"cwe"` + Path string `json:"path"` // repository-relative, slash-separated + StartLine int `json:"startLine"` + EndLine int `json:"endLine"` + Symbol string `json:"symbol"` // the enclosing symbol, e.g. "Server.handle"; empty at top level + Snippet string `json:"snippet"` + // Context is the match with ContextLines of source either side, for a + // reader and for the coding agent; untrusted, like Snippet. + ContextStartLine int `json:"contextStartLine"` + ContextEndLine int `json:"contextEndLine"` + Context string `json:"context"` + Provenance Provenance `json:"provenance"` +} + +// ContextLines is how many lines of source surround a match in its context. +const ContextLines = 3 + +// EnclosingSymbolPath is the anvil-fp/v1 SAST tier's enclosing_symbol_path: +// "path::Symbol", or empty when the match has no enclosing symbol. +func (c Candidate) EnclosingSymbolPath() string { + if c.Symbol == "" { + return "" + } + return c.Path + "::" + c.Symbol +} + +// Result is one scan's output. +type Result struct { + Candidates []Candidate + // Count is len(Candidates): the candidates-per-scan metric. + Count int + // ByTool counts candidates per tool. + ByTool map[string]int + // Files is how many files Lane B handed a tool; ToolsRun the tools it ran. + Files int + ToolsRun []string + // Problems are coverage failures. Any problem makes the scan incomplete, + // which the store records as a partial scan that resolves nothing. + Problems []string + // Notes are informational and do not make the scan incomplete. + Notes []string + // PartialParses counts files opengrep parsed only in part. This is the + // engine's ordinary behaviour on macro-heavy C (144 of curl's 495 files on + // 2026-10-03), so it is reported rather than treated as incomplete. + PartialParses int +} + +// Scanner runs the recall tier. +type Scanner struct { + Pack *Pack + Tools Tools + Exec Exec // nil means ProcessExec +} + +// Plan is what a scan of one tree will run, decided before anything runs. +type Plan struct { + Root string + // Targets are the absolute paths of the files handed to each tool. + Opengrep []string + Bandit []string + // Modules are the Go module roots gosec runs in. + Modules []string + // GoFiles counts, per module root, the non-test Go files the Go tool + // would load there: gosec is expected to analyse each one. Orphans are Go + // files in no module, and Vendored those under a vendor/ directory. + GoFiles map[string]int + Orphans int + Vendored int + // Excluded is how many source files the selection's excluded paths kept + // out. + Excluded int + + bins map[string]string +} + +// Needs lists the tools the plan runs. +func (p Plan) Needs() []string { + var out []string + if len(p.Opengrep) > 0 { + out = append(out, ToolOpengrep) + } + if len(p.Modules) > 0 { + out = append(out, ToolGosec) + } + if len(p.Bandit) > 0 { + out = append(out, ToolBandit) + } + return out +} + +// Only narrows a plan to the named repository-relative files, for a scan of +// what one push changed. Gosec still loads whole packages (it cannot do +// less), so its matches outside the named files are dropped afterwards by +// whoever asked for the narrowing. +func (p Plan) Only(rel []string) Plan { + want := map[string]bool{} + for _, r := range rel { + want[filepath.Join(p.Root, filepath.FromSlash(r))] = true + } + keep := func(in []string) []string { + var out []string + for _, f := range in { + if want[f] { + out = append(out, f) + } + } + return out + } + q := p + q.Opengrep, q.Bandit = keep(p.Opengrep), keep(p.Bandit) + goChanged := false + for r := range want { + goChanged = goChanged || strings.HasSuffix(r, ".go") + } + if !goChanged { + q.Modules = nil + } + return q +} + +// Empty reports whether the tree holds no file any recall tool reads. +func (p Plan) Empty() bool { return len(p.Needs()) == 0 } + +// extensions maps a file extension to the rule languages that read it. +var extensions = map[string][]string{ + ".c": {"c", "cpp"}, ".h": {"c", "cpp"}, + ".cc": {"cpp"}, ".cpp": {"cpp"}, ".cxx": {"cpp"}, ".hpp": {"cpp"}, ".hh": {"cpp"}, ".hxx": {"cpp"}, + ".py": {"python"}, ".pyi": {"python"}, + ".go": {"go"}, + ".java": {"java"}, + ".js": {"javascript"}, ".jsx": {"javascript"}, ".mjs": {"javascript"}, ".cjs": {"javascript"}, + ".ts": {"typescript"}, ".tsx": {"typescript"}, + ".cs": {"csharp"}, + ".scala": {"scala"}, +} + +// Prepare enumerates root and decides which tools the scan needs, then +// resolves each one and checks its version against the pin. It runs nothing +// over the repository, so a missing tool stops a scan before it starts. +// +// Enumeration never follows a symbolic link and never enters .git: a link is +// a path out of the tree under scan. +func (s Scanner) Prepare(ctx context.Context, root string) (Plan, error) { + if s.Pack == nil { + return Plan{}, fmt.Errorf("%w: no rule pack", ErrRulePack) + } + abs, err := filepath.Abs(root) + if err != nil { + return Plan{}, err + } + plan := Plan{Root: abs, bins: map[string]string{}, GoFiles: map[string]int{}} + langs := map[string]bool{} + for _, l := range s.Pack.Languages() { + langs[l] = true + } + var goFiles []string // non-test Go files the Go tool would load, absolute + var modules []string + err = filepath.WalkDir(abs, func(full string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, _ := filepath.Rel(abs, full) + rel = filepath.ToSlash(rel) + if d.IsDir() { + if d.Name() == ".git" && full != abs { + return filepath.SkipDir + } + return nil + } + if d.Type().IsRegular() && strings.HasSuffix(d.Name(), ".go") && !strings.HasSuffix(d.Name(), "_test.go") { + if goToolSees(rel) { + goFiles = append(goFiles, full) + } else if inVendor(rel) { + plan.Vendored++ + } + } + if !d.Type().IsRegular() { + return nil + } + if d.Name() == "go.mod" { + modules = append(modules, filepath.Dir(full)) + return nil + } + ls, ok := extensions[strings.ToLower(filepath.Ext(d.Name()))] + if !ok { + return nil + } + if s.Pack.Excluded(rel) { + plan.Excluded++ + return nil + } + ruled := false + for _, l := range ls { + ruled = ruled || langs[l] + } + if ruled { + plan.Opengrep = append(plan.Opengrep, full) + } + if filepath.Ext(d.Name()) == ".py" { + plan.Bandit = append(plan.Bandit, full) + } + return nil + }) + if err != nil { + return Plan{}, fmt.Errorf("recall: enumerating %s: %w", abs, err) + } + if len(goFiles) > 0 { + plan.Modules = modules + for _, f := range goFiles { + if m := owningModule(f, modules); m != "" { + plan.GoFiles[m]++ + } else { + plan.Orphans++ + } + } + } + x := s.exec() + for _, tool := range plan.Needs() { + bin, err := resolve(tool, s.Tools.binary(tool)) + if err != nil { + return plan, err + } + pin, ok := s.Pack.Tool(tool) + if !ok { + return plan, fmt.Errorf("%w: the selection pins no %s release", ErrRulePack, tool) + } + if err := checkVersion(ctx, x, tool, bin, pin.Version); err != nil { + return plan, err + } + plan.bins[tool] = bin + } + return plan, nil +} + +func (s Scanner) exec() Exec { + if s.Exec == nil { + return ProcessExec{} + } + return s.Exec +} + +// Scan runs a prepared plan. A tool that fails fails the scan: a candidate +// list missing one tool's matches would read as that tool finding nothing. +func (s Scanner) Scan(ctx context.Context, plan Plan) (Result, error) { + res := Result{ByTool: map[string]int{}, ToolsRun: plan.Needs(), Files: len(plan.Opengrep)} + if plan.Empty() { + return res, nil + } + x := s.exec() + var runs []toolRun + if len(plan.Opengrep) > 0 { + rules := make([]string, 0, len(s.Pack.Rules())) + for _, r := range s.Pack.Rules() { + p, err := filepath.Abs(filepath.Join(s.Pack.Root, filepath.FromSlash(r.Path))) + if err != nil { + return res, err + } + rules = append(rules, p) + } + run, err := runOpengrep(ctx, x, plan.bins[ToolOpengrep], rules, plan.Opengrep) + if err != nil { + return res, err + } + // Every file handed to opengrep must be one it says it scanned. A file + // it skipped (over its size limit, unreadable) is a hole in coverage. + missed := 0 + for _, t := range plan.Opengrep { + if !run.scanned[t] { + missed++ + } + } + if missed > 0 { + run.problems = append(run.problems, fmt.Sprintf("opengrep did not scan %d of the %d files it was given", missed, len(plan.Opengrep))) + } + runs = append(runs, run) + } + if len(plan.Modules) > 0 { + run, err := runGosec(ctx, x, plan.bins[ToolGosec], s.Tools.GoBin, plan.Modules) + if err != nil { + return res, err + } + // gosec analyses only the files the Go tool loads for this platform + // with cgo off; one it left out (build-constrained for another OS, + // cgo, unloadable) was read by opengrep alone, and the scan says so. + for _, m := range plan.Modules { + if want, got := plan.GoFiles[m], run.analysed[m]; got < want { + run.problems = append(run.problems, fmt.Sprintf( + "gosec analysed %d of the %d Go files in module %s; the rest (another platform's build constraints, cgo, or a package it could not load) were read by opengrep only", + got, want, relOrSelf(plan.Root, m))) + } + } + runs = append(runs, run) + } + if plan.Orphans > 0 { + res.Problems = append(res.Problems, fmt.Sprintf( + "%d Go file(s) belong to no Go module, so gosec did not analyse them (opengrep did)", plan.Orphans)) + } + if plan.Vendored > 0 { + res.Notes = append(res.Notes, fmt.Sprintf( + "%d Go file(s) under vendor/ were read by opengrep only; gosec does not analyse vendored packages", plan.Vendored)) + } + if len(plan.Bandit) > 0 { + run, err := runBandit(ctx, x, plan.bins[ToolBandit], plan.Bandit) + if err != nil { + return res, err + } + missed := 0 + for _, t := range plan.Bandit { + if !run.scanned[t] { + missed++ + } + } + if missed > 0 { + run.problems = append(run.problems, fmt.Sprintf("bandit did not scan %d of the %d files it was given", missed, len(plan.Bandit))) + } + runs = append(runs, run) + } + + files := newSourceCache(plan.Root) + var cands []Candidate + for _, run := range runs { + res.Problems = append(res.Problems, run.problems...) + res.PartialParses += run.partialParses + for _, f := range run.findings { + c, keep, err := s.candidate(plan.Root, f, files) + var notRegular *notRegularError + if errors.As(err, ¬Regular) { + // A tool followed a symbolic link Lane B does not follow + // (gosec loads a linked .go file as part of its package). + // The match is not reported, and the scan says so. + res.Problems = append(res.Problems, fmt.Sprintf( + "%s reported a match in %s, which is not a regular file in the tree; Lane B does not follow links", f.tool, notRegular.rel)) + continue + } + if err != nil { + return res, err + } + if keep { + cands = append(cands, c) + } + } + } + res.Candidates = Dedupe(cands) + res.Count = len(res.Candidates) + for _, c := range res.Candidates { + res.ByTool[c.Tool]++ + } + return res, nil +} + +// candidate turns one tool finding into a candidate, or drops it (keep false) +// when it lies outside the tree, in an excluded path, or under an excluded +// tool rule. +func (s Scanner) candidate(root string, f rawFinding, files *sourceCache) (Candidate, bool, error) { + abs := f.absPath + if !filepath.IsAbs(abs) { + abs = filepath.Join(root, abs) + } + rel, err := filepath.Rel(root, abs) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return Candidate{}, false, nil + } + rel = filepath.ToSlash(rel) + if s.Pack.Excluded(rel) { + return Candidate{}, false, nil + } + c := Candidate{Tool: f.tool, Path: rel, StartLine: f.startLine, EndLine: f.endLine} + if c.EndLine < c.StartLine { + c.EndLine = c.StartLine + } + switch f.tool { + case ToolOpengrep: + r, id, ok := s.Pack.RuleByID(f.rule) + if !ok { + return c, false, &ToolFailedError{Tool: ToolOpengrep, Reason: fmt.Sprintf("reported rule %q, which resolves to no single rule in the pack", f.rule)} + } + c.RuleID = id + c.RuleIDVersioned = r.Corpus + "/" + c.RuleID + "@" + r.BlobSHA1[:12] + c.CWE = r.CWEs[0] + c.Provenance = Provenance{ + Source: r.Corpus, Repository: r.Repository, Version: r.Commit, RulePath: r.Path, + Licence: r.Licence, LicenceEvidence: r.LicenceEvidence, + } + c.Symbol = strings.Join(f.enclosing, ".") + default: + if s.Pack.ToolRuleExcluded(f.tool, f.rule) { + return Candidate{}, false, nil + } + pin, _ := s.Pack.Tool(f.tool) + c.RuleID = f.rule + c.RuleIDVersioned = f.tool + "/" + f.rule + "@" + pin.Version + c.CWE = f.cwe + if c.CWE == "" { + return c, false, &ToolFailedError{Tool: f.tool, Reason: fmt.Sprintf("rule %s reported no CWE; every candidate needs one", f.rule)} + } + c.Provenance = Provenance{ + Source: f.tool, Repository: pin.Repository, Version: pin.Version, RulePath: f.tool + ":" + f.rule, + Licence: pin.Licence, LicenceEvidence: pin.LicenceEvidence, + } + } + lines, err := files.lines(rel) + if errors.Is(err, errNotRegular) { + return c, false, ¬RegularError{rel: rel} + } + if err != nil { + return c, false, fmt.Errorf("recall: reading %s for a %s match: %w", rel, f.tool, err) + } + c.Snippet = snippet(lines, c.StartLine, c.EndLine, maxSnippetLines) + c.ContextStartLine = max(1, c.StartLine-ContextLines) + c.ContextEndLine = min(len(lines), c.EndLine+ContextLines) + c.Context = snippet(lines, c.ContextStartLine, c.ContextEndLine, maxSnippetLines+2*ContextLines) + // One naming per language, whichever tool matched: a Go method is + // "Recv.Method" from go/parser for gosec and opengrep alike (opengrep's + // own context drops the receiver), and bandit's matches are read from + // Python's indentation. + switch { + case strings.HasSuffix(rel, ".go"): + c.Symbol = goSymbol(files.source(rel), c.StartLine) + case f.tool == ToolBandit: + c.Symbol = pythonSymbol(lines, c.StartLine) + } + if err := c.check(); err != nil { + return c, false, err + } + return c, true, nil +} + +// check is the contract: 100% non-empty provenance, and the fields the +// fingerprint and the record need. +func (c Candidate) check() error { + p := c.Provenance + for name, v := range map[string]string{ + "rule id": c.RuleIDVersioned, "CWE": c.CWE, "path": c.Path, "snippet": strings.TrimSpace(c.Snippet), + "provenance source": p.Source, "provenance repository": p.Repository, "provenance version": p.Version, + "provenance rule path": p.RulePath, "provenance licence": p.Licence, "provenance licence evidence": p.LicenceEvidence, + } { + if v == "" { + return fmt.Errorf("recall: a %s candidate at %s:%d has no %s", c.Tool, c.Path, c.StartLine, name) + } + } + if c.StartLine < 1 { + return fmt.Errorf("recall: a %s candidate at %s has no line", c.Tool, c.Path) + } + return nil +} + +// toolOrder is the precedence Dedupe applies when two tools match the same +// line with the same CWE: a native analyser, which has type or AST +// information, over a pattern rule. +var toolOrder = map[string]int{ToolGosec: 0, ToolBandit: 1, ToolOpengrep: 2} + +// Dedupe keeps one candidate per (path, line, CWE), the unit the +// candidates-per-scan instrument counts, choosing deterministically: tool +// precedence, then the rule id. It returns them sorted by path, line, CWE. +func Dedupe(cands []Candidate) []Candidate { + sorted := append([]Candidate(nil), cands...) + sort.SliceStable(sorted, func(i, j int) bool { + a, b := sorted[i], sorted[j] + if a.Path != b.Path { + return a.Path < b.Path + } + if a.StartLine != b.StartLine { + return a.StartLine < b.StartLine + } + if a.CWE != b.CWE { + return a.CWE < b.CWE + } + if toolOrder[a.Tool] != toolOrder[b.Tool] { + return toolOrder[a.Tool] < toolOrder[b.Tool] + } + return a.RuleIDVersioned < b.RuleIDVersioned + }) + out := sorted[:0] + for i, c := range sorted { + if i > 0 { + p := out[len(out)-1] + if p.Path == c.Path && p.StartLine == c.StartLine && p.CWE == c.CWE { + continue + } + } + out = append(out, c) + } + return out +} + +// maxSnippetLines bounds a snippet. A match spanning more lines than this is +// cut, and says so in its last line. +const maxSnippetLines = 40 + +func snippet(lines []string, start, end, maxLines int) string { + if start < 1 || start > len(lines) { + return "" + } + if end > len(lines) { + end = len(lines) + } + cut := false + if end-start+1 > maxLines { + end, cut = start+maxLines-1, true + } + s := strings.Join(lines[start-1:end], "\n") + if cut { + s += "\n…" + } + return s +} + +var errNotRegular = errors.New("not a regular file") + +// notRegularError is a tool match in a path that is not a regular file in +// the tree: a symbolic link, most often. +type notRegularError struct{ rel string } + +func (e *notRegularError) Error() string { return "recall: " + e.rel + " is not a regular file" } + +// goToolSees reports whether the Go tool loads a file at this relative path: +// not under testdata/ or vendor/, nor under a directory whose name starts +// with "." or "_". +func goToolSees(rel string) bool { + segs := strings.Split(rel, "/") + for _, s := range segs[:len(segs)-1] { + if s == "testdata" || s == "vendor" || strings.HasPrefix(s, ".") || strings.HasPrefix(s, "_") { + return false + } + } + return !strings.HasPrefix(segs[len(segs)-1], ".") && !strings.HasPrefix(segs[len(segs)-1], "_") +} + +func inVendor(rel string) bool { + return rel == "vendor" || strings.HasPrefix(rel, "vendor/") || strings.Contains(rel, "/vendor/") +} + +// owningModule is the deepest module root containing file, or "". +func owningModule(file string, modules []string) string { + best := "" + for _, m := range modules { + if strings.HasPrefix(file, m+string(filepath.Separator)) && len(m) > len(best) { + best = m + } + } + return best +} + +// sourceCache reads each file once. +type sourceCache struct { + root string + files map[string][]string + raw map[string][]byte +} + +func newSourceCache(root string) *sourceCache { + return &sourceCache{root: root, files: map[string][]string{}, raw: map[string][]byte{}} +} + +func (c *sourceCache) lines(rel string) ([]string, error) { + if l, ok := c.files[rel]; ok { + return l, nil + } + full := filepath.Join(c.root, filepath.FromSlash(rel)) + st, err := os.Lstat(full) + if err != nil { + return nil, err + } + if !st.Mode().IsRegular() { + return nil, errNotRegular + } + b, err := os.ReadFile(full) + if err != nil { + return nil, err + } + var out []string + sc := bufio.NewScanner(strings.NewReader(string(b))) + sc.Buffer(make([]byte, 0, 64<<10), 16<<20) + for sc.Scan() { + out = append(out, strings.TrimRight(sc.Text(), "\r")) + } + if err := sc.Err(); err != nil { + return nil, err + } + c.files[rel], c.raw[rel] = out, b + return out, nil +} + +func (c *sourceCache) source(rel string) []byte { return c.raw[rel] } diff --git a/internal/recall/doc.go b/internal/recall/doc.go new file mode 100644 index 0000000..6f1e4dc --- /dev/null +++ b/internal/recall/doc.go @@ -0,0 +1,6 @@ +// Package recall is Lane B's recall tier (plan nodes recall, opengrep and +// candidatelist): it runs the vendored rule corpora through opengrep, and gosec +// and bandit natively, each as a subprocess, and turns what they report into +// candidates that carry their rule's provenance. It decides nothing about a +// candidate: every one is a rule match nobody has judged. +package recall diff --git a/internal/recall/recall_test.go b/internal/recall/recall_test.go new file mode 100644 index 0000000..ac8ef50 --- /dev/null +++ b/internal/recall/recall_test.go @@ -0,0 +1,572 @@ +package recall_test + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "io/fs" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/Susquehanna-Syntax/Anvil/internal/recall" + "github.com/Susquehanna-Syntax/Anvil/internal/recall/recalltest" +) + +func pack(t *testing.T) *recall.Pack { + t.Helper() + p, err := recall.LoadPack(recalltest.RulePack()) + if err != nil { + t.Fatalf("the vendored rule pack does not verify: %v", err) + } + return p +} + +// TestTheVendoredPackVerifies is the pack as committed: every rule and licence +// body hashes to the manifest, the manifest names this selection, and every +// rule's provenance is complete. +func TestTheVendoredPackVerifies(t *testing.T) { + p := pack(t) + if n := len(p.Rules()); n != 176 { + t.Errorf("the pack holds %d rule files; the owner's selection of 2026-10-03 vendors 176", n) + } + if n := len(p.Manifest.ExcludedByLicence); n != 131 { + t.Errorf("the manifest lists %d rules excluded for an LGPL-3.0 upstream; the generator left out 131", n) + } + for _, r := range p.Rules() { + if !slices.Contains(recall.AdmittedLicences, r.Licence) || r.LicenceEvidence == "" || r.Commit == "" || r.Repository == "" { + t.Errorf("%s: incomplete provenance %+v", r.Path, r) + } + if strings.HasPrefix(r.Path, "gitlab-sast-rules/c/") { + t.Errorf("%s: GitLab's c/ rules are GPL-2.0 and excluded", r.Path) + } + } + for _, tool := range []string{recall.ToolOpengrep, recall.ToolGosec, recall.ToolBandit} { + if pin, ok := p.Tool(tool); !ok || pin.Version == "" || pin.Licence == "" { + t.Errorf("no pin for %s: %+v", tool, pin) + } + } +} + +// copyPack copies data/rules to a temporary directory a test may mutate. +func copyPack(t *testing.T) string { + t.Helper() + src, dst := recalltest.RulePack(), t.TempDir() + err := filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, _ := filepath.Rel(src, p) + if d.IsDir() { + return os.MkdirAll(filepath.Join(dst, rel), 0o755) + } + b, err := os.ReadFile(p) + if err != nil { + return err + } + return os.WriteFile(filepath.Join(dst, rel), b, 0o644) + }) + if err != nil { + t.Fatal(err) + } + return dst +} + +func editJSON(t *testing.T, path string, edit func(map[string]any)) { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var doc map[string]any + if err := json.Unmarshal(raw, &doc); err != nil { + t.Fatal(err) + } + edit(doc) + out, err := json.MarshalIndent(doc, "", " ") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, out, 0o644); err != nil { + t.Fatal(err) + } +} + +// resign makes the manifest name the selection on disk again, so a mutation +// to the selection reaches the check it is aimed at instead of tripping the +// digest check first. +func resign(t *testing.T, dir string) { + t.Helper() + raw, err := os.ReadFile(filepath.Join(dir, "selection.json")) + if err != nil { + t.Fatal(err) + } + editJSON(t, filepath.Join(dir, "MANIFEST.json"), func(m map[string]any) { + m["selection_sha256"] = sha256Hex(raw) + }) +} + +// TestThePackGuardsFire is the negative control for every pack check: each +// mutation is a way a rule could reach Lane B without review, and each must +// be refused. The GitLab directory allowlist is the first two. +func TestThePackGuardsFire(t *testing.T) { + firstRule := func(m map[string]any) map[string]any { + return m["rules"].([]any)[0].(map[string]any) + } + cases := map[string]func(t *testing.T, dir string){ + "a GitLab directory off the allowlist (c/, GPL-2.0)": func(t *testing.T, dir string) { + editJSON(t, filepath.Join(dir, "selection.json"), func(m map[string]any) { + c := m["corpora"].([]any)[0].(map[string]any) + c["directories"] = append(c["directories"].([]any), "c") + m["excluded_directories"] = m["excluded_directories"].([]any)[1:] + }) + resign(t, dir) + }, + "a GitLab directory that is a licence trap (doc/, CC BY-SA 4.0)": func(t *testing.T, dir string) { + editJSON(t, filepath.Join(dir, "selection.json"), func(m map[string]any) { + c := m["corpora"].([]any)[0].(map[string]any) + c["directories"] = append(c["directories"].([]any), "doc") + }) + resign(t, dir) + }, + "a corpus from a repository that is not admitted": func(t *testing.T, dir string) { + editJSON(t, filepath.Join(dir, "selection.json"), func(m map[string]any) { + m["corpora"].([]any)[1].(map[string]any)["repository"] = "https://github.com/semgrep/semgrep-rules" + }) + resign(t, dir) + }, + "a rule whose licence is not admitted": func(t *testing.T, dir string) { + editJSON(t, filepath.Join(dir, "MANIFEST.json"), func(m map[string]any) { + firstRule(m)["licence"] = "GPL-2.0" + }) + }, + "a rule with no licence evidence": func(t *testing.T, dir string) { + editJSON(t, filepath.Join(dir, "MANIFEST.json"), func(m map[string]any) { + firstRule(m)["licence_evidence"] = " " + }) + }, + "a rule file edited after vendoring": func(t *testing.T, dir string) { + var first string + editJSON(t, filepath.Join(dir, "MANIFEST.json"), func(m map[string]any) { first = firstRule(m)["path"].(string) }) + f := filepath.Join(dir, filepath.FromSlash(first)) + b, _ := os.ReadFile(f) + if err := os.WriteFile(f, append(b, '\n'), 0o644); err != nil { + t.Fatal(err) + } + }, + "an unlisted rule file beside the vendored ones": func(t *testing.T, dir string) { + if err := os.WriteFile(filepath.Join(dir, "gitlab-sast-rules", "python", "extra.yml"), []byte("rules: []\n"), 0o644); err != nil { + t.Fatal(err) + } + }, + "a selection the manifest was not generated from": func(t *testing.T, dir string) { + editJSON(t, filepath.Join(dir, "selection.json"), func(m map[string]any) { m["decided_by"] = "someone else" }) + }, + "a licence body edited after archiving": func(t *testing.T, dir string) { + f := filepath.Join(dir, "0xdea-semgrep-rules", "LICENSE") + b, _ := os.ReadFile(f) + if err := os.WriteFile(f, append(b, ' '), 0o644); err != nil { + t.Fatal(err) + } + }, + "a corpus pinned to a branch, not a commit": func(t *testing.T, dir string) { + editJSON(t, filepath.Join(dir, "selection.json"), func(m map[string]any) { + m["corpora"].([]any)[0].(map[string]any)["commit"] = "main" + }) + resign(t, dir) + }, + } + for name, mutate := range cases { + t.Run(name, func(t *testing.T) { + dir := copyPack(t) + if _, err := recall.LoadPack(dir); err != nil { + t.Fatalf("the unmutated copy does not load: %v", err) + } + mutate(t, dir) + if _, err := recall.LoadPack(dir); !errors.Is(err, recall.ErrRulePack) { + t.Fatalf("the mutated pack loaded (err %v)", err) + } + }) + } +} + +func TestExcludedPaths(t *testing.T) { + p := pack(t) + for path, want := range map[string]bool{ + "tests/test_x.py": true, "src/tests/x.c": true, "docs/examples/a.c": true, "pkg/x_test.go": true, + "pkg/testdata/a.go": true, "conftest.py": true, "lib/test_util.py": true, + "src/app.py": false, "lib/testing.c": false, "src/latest/x.go": false, "contest.py": false, + } { + if got := p.Excluded(path); got != want { + t.Errorf("Excluded(%q) = %v, want %v", path, got, want) + } + } +} + +// TestTheArgumentVectorsSwitchOffTargetSuppression holds the flags that keep +// the scanned repository from silencing Lane B, and the absence of the flag +// that would make gosec's silence look like a clean scan. +func TestTheArgumentVectorsSwitchOffTargetSuppression(t *testing.T) { + og := recall.OpengrepArgs([]string{"/r/a.yml"}, []string{"/repo/a.py"}) + for _, want := range []string{"--disable-nosem", "--disable-version-check", "--timeout-threshold=0", "--config=/r/a.yml"} { + if !slices.Contains(og, want) { + t.Errorf("opengrep args %v lack %s", og, want) + } + } + if og[len(og)-1] != "/repo/a.py" { + t.Errorf("opengrep's targets must be the files handed to it: %v", og) + } + gs := recall.GosecArgs("/tmp/out.json") + if !slices.Contains(gs, "-nosec") || slices.Contains(gs, "-quiet") || !slices.Contains(gs, "-no-fail") { + t.Errorf("gosec args %v", gs) + } + bd := recall.BanditArgs([]string{"/repo/a.py"}, "/tmp/out.json") + if !slices.Contains(bd, "--ignore-nosec") || slices.Contains(bd, "-r") { + t.Errorf("bandit args %v", bd) + } + env := strings.Join(recall.GosecEnv(""), "\n") + for _, want := range []string{"GOPROXY=off", "GOSUMDB=off", "GOTOOLCHAIN=local", "GOFLAGS=-mod=readonly"} { + if !strings.Contains(env, want) { + t.Errorf("gosec's environment lacks %s", want) + } + } +} + +// fakeExec answers version checks with the pins and every scan with fixed +// output, exit code and report bytes. +type fakeExec struct { + stdout string + code int + report string // written to the report file gosec and bandit are given + version map[string]string + runError error +} + +func (f fakeExec) Run(_ context.Context, bin string, args []string, _ string, _ []string) ([]byte, []byte, int, error) { + tool := filepath.Base(bin) + if len(args) == 1 && strings.Contains(args[0], "version") { + v := map[string]string{"opengrep": "1.26.0", "gosec": "Version: 2.29.0", "bandit": "bandit 1.9.4"}[tool] + if f.version != nil { + v = f.version[tool] + } + return []byte(v + "\n"), nil, 0, nil + } + if f.runError != nil { + return nil, nil, 0, f.runError + } + for i, a := range args { + if out, ok := strings.CutPrefix(a, "-out="); ok && f.report != "" { + _ = os.WriteFile(out, []byte(f.report), 0o600) + } + if a == "-o" && i+1 < len(args) && f.report != "" { + _ = os.WriteFile(args[i+1], []byte(f.report), 0o600) + } + } + return []byte(f.stdout), []byte("stderr text"), f.code, nil +} + +// oneFile is a tree with a single Python file, which needs opengrep and bandit. +func oneFile(t *testing.T, name, body string) string { + t.Helper() + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, name), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + return root +} + +// TestAbsentFailedAndNoMatchesAreThreeAnswers is the three-way distinction +// every runner owes: an absent tool, a failed tool and a clean run never look +// alike. +func TestAbsentFailedAndNoMatchesAreThreeAnswers(t *testing.T) { + ctx := context.Background() + p := pack(t) + root := oneFile(t, "a.go", "package a\n") + if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module a\n"), 0o644); err != nil { + t.Fatal(err) + } + tools := recalltest.Tools() + + t.Run("absent", func(t *testing.T) { + s := recall.Scanner{Pack: p, Tools: recall.Tools{Opengrep: filepath.Join(t.TempDir(), "opengrep"), Gosec: tools.Gosec}, Exec: fakeExec{}} + _, err := s.Prepare(ctx, root) + var absent *recall.ToolAbsentError + if !errors.As(err, &absent) || absent.ExitCode() != recall.ExitCodeArtefactAbsent { + t.Fatalf("an absent opengrep gave %v", err) + } + }) + t.Run("wrong version", func(t *testing.T) { + s := recall.Scanner{Pack: p, Tools: tools, Exec: fakeExec{version: map[string]string{"opengrep": "1.25.0", "gosec": "Version: 2.29.0"}}} + if _, err := s.Prepare(ctx, root); !errors.Is(err, recall.ErrToolFailed) { + t.Fatalf("an unpinned opengrep release gave %v", err) + } + }) + scan := func(t *testing.T, x recall.Exec) (recall.Result, error) { + t.Helper() + s := recall.Scanner{Pack: p, Tools: tools, Exec: x} + plan, err := s.Prepare(ctx, root) + if err != nil { + t.Fatal(err) + } + return s.Scan(ctx, plan) + } + cleanOpengrep := `{"results":[],"errors":[],"paths":{"scanned":["` + filepath.Join(root, "a.go") + `"]}}` + cleanGosec := `{"Golang errors":{},"Issues":[],"Stats":{"files":1}}` + for name, x := range map[string]fakeExec{ + "opengrep exits 2": {stdout: cleanOpengrep, code: 2}, + "opengrep prints nothing": {stdout: ""}, + "opengrep prints half a report": {stdout: `{"results":[`}, + "opengrep's report has no paths": {stdout: `{"results":[],"errors":[]}`}, + "opengrep could not be run": {runError: errors.New("exec format error")}, + "gosec writes nothing (as -quiet did)": {stdout: cleanOpengrep}, + } { + t.Run(name, func(t *testing.T) { + if _, err := scan(t, x); !errors.Is(err, recall.ErrToolFailed) { + t.Fatalf("got %v, want a failed tool", err) + } + }) + } + t.Run("no matches", func(t *testing.T) { + res, err := scan(t, fakeExec{stdout: cleanOpengrep, report: cleanGosec}) + if err != nil || res.Count != 0 || len(res.Problems) != 0 { + t.Fatalf("a clean run: %+v, %v", res, err) + } + }) + t.Run("a file handed over and not scanned is a coverage problem", func(t *testing.T) { + res, err := scan(t, fakeExec{stdout: `{"results":[],"errors":[],"paths":{"scanned":[]}}`, report: cleanGosec}) + if err != nil || len(res.Problems) != 1 { + t.Fatalf("got %+v, %v; want one problem", res.Problems, err) + } + }) + t.Run("a package gosec cannot load is a coverage problem", func(t *testing.T) { + res, err := scan(t, fakeExec{stdout: cleanOpengrep, report: `{"Golang errors":{"` + filepath.Join(root, "a.go") + `":[{"line":1,"error":"no module"}]},"Issues":[],"Stats":{"files":1}}`}) + if err != nil || len(res.Problems) != 1 || !strings.Contains(res.Problems[0], "gosec could not load") { + t.Fatalf("got %+v, %v", res.Problems, err) + } + }) +} + +// expectedFixture is every candidate testdata/laneb-fixture must yield: the +// planted matches, the suppressed one still reported, nothing from tests/. +var expectedFixture = []string{ + "gitlab-sast-rules/java_crypto_rule-CipherDESInsecure src/Digest.java:6 Digest.cipher", + "gitlab-sast-rules/java_crypto_rule-CipherIntegrity src/Digest.java:6 Digest.cipher", + "bandit/B404 src/app.py:2 ", + "bandit/B307 src/app.py:7 run_expression", + "gitlab-sast-rules/python_eval_rule-eval src/app.py:7 run_expression", + "bandit/B602 src/app.py:11 run_command", + "0xdea-semgrep-rules/raptor-write-into-stack-buffer src/copy.c:6 copy", + "0xdea-semgrep-rules/raptor-insecure-api-strcpy-strcat src/copy.c:6 copy", + "gosec/G204 src/main.go:12 runner.run", + "bandit/B307 src/suppressed.py:6 run_expression", + "gitlab-sast-rules/python_eval_rule-eval src/suppressed.py:6 run_expression", + "gitlab-sast-rules/javascript_require_rule-non-literal-require web/app.js:3 load", +} + +func describe(res recall.Result) []string { + var out []string + for _, c := range res.Candidates { + id := c.RuleIDVersioned[:strings.LastIndex(c.RuleIDVersioned, "@")] + out = append(out, id+" "+c.Path+":"+itoa(c.StartLine)+" "+c.Symbol) + } + return out +} + +func itoa(n int) string { b, _ := json.Marshal(n); return string(b) } + +func checkFixture(t *testing.T, res recall.Result, plan recall.Plan) { + t.Helper() + if got := describe(res); !slices.Equal(got, expectedFixture) { + t.Fatalf("fixture candidates:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(expectedFixture, "\n")) + } + if res.Count != len(expectedFixture) || len(res.Problems) != 0 { + t.Fatalf("count %d, problems %v", res.Count, res.Problems) + } + if plan.Excluded != 1 { + t.Errorf("the selection's paths excluded %d files, want 1 (tests/test_planted.py)", plan.Excluded) + } + for _, c := range res.Candidates { + p := c.Provenance + if p.Source == "" || p.Repository == "" || p.Version == "" || p.RulePath == "" || p.Licence == "" || p.LicenceEvidence == "" { + t.Errorf("%s at %s:%d has incomplete provenance %+v", c.RuleIDVersioned, c.Path, c.StartLine, p) + } + if c.CWE == "" || strings.TrimSpace(c.Snippet) == "" || c.Context == "" { + t.Errorf("%s at %s:%d lacks a CWE, snippet or context", c.RuleIDVersioned, c.Path, c.StartLine) + } + } +} + +// TestTheFixtureFromRecordedReports runs the whole recall tier over the +// fixture with the tools' real reports replayed, so it runs where the tools +// are not installed. TestTheFixtureWithTheRealTools proves the recording. +func TestTheFixtureFromRecordedReports(t *testing.T) { + root, err := filepath.Abs(recalltest.FixtureRoot()) + if err != nil { + t.Fatal(err) + } + replay := &recalltest.Replay{Root: root} + s := recall.Scanner{Pack: pack(t), Tools: recalltest.Tools(), Exec: replay} + plan, err := s.Prepare(context.Background(), root) + if err != nil { + t.Fatal(err) + } + res, err := s.Scan(context.Background(), plan) + if err != nil { + t.Fatal(err) + } + checkFixture(t, res, plan) + // Every target handed to opengrep and bandit is a file, never the root. + for _, call := range replay.Calls { + if slices.Contains(call, root) { + t.Errorf("a tool was handed the tree's root as a target: %v", call) + } + } +} + +// realTools is the tools from ANVIL_OPENGREP, ANVIL_GOSEC, ANVIL_BANDIT and +// ANVIL_GOBIN, or their names on PATH. +func realTools() recall.Tools { + return recall.Tools{Opengrep: os.Getenv("ANVIL_OPENGREP"), Gosec: os.Getenv("ANVIL_GOSEC"), + Bandit: os.Getenv("ANVIL_BANDIT"), GoBin: os.Getenv("ANVIL_GOBIN")} +} + +// TestTheFixtureWithTheRealTools runs the real, pinned tools over the +// fixture. Where they are absent the scan must refuse as a missing tool, never +// come back empty; with ANVIL_LANEB_E2E=1 (CI's Lane B end-to-end job) an +// absent tool fails the test. To re-record the replayed reports after a +// fixture or rule change, run it with ANVIL_LANEB_RECORD=1. +func TestTheFixtureWithTheRealTools(t *testing.T) { + root, err := filepath.Abs(recalltest.FixtureRoot()) + if err != nil { + t.Fatal(err) + } + var x recall.Exec = recall.ProcessExec{} + if os.Getenv("ANVIL_LANEB_RECORD") == "1" { + x = recalltest.Recorder{Root: root, Real: x} + } + tools := realTools() + if tools.GoBin == "" { + if goBin, err := exec.LookPath("go"); err == nil { + tools.GoBin = filepath.Dir(goBin) + } + } + s := recall.Scanner{Pack: pack(t), Tools: tools, Exec: x} + plan, err := s.Prepare(context.Background(), root) + if errors.Is(err, recall.ErrToolAbsent) { + if os.Getenv("ANVIL_LANEB_E2E") == "1" { + t.Fatalf("ANVIL_LANEB_E2E=1 and a recall tool is absent: %v", err) + } + t.Logf("a recall tool is not installed, and the scan refused as a missing tool, as it must: %v", err) + return + } + if err != nil { + t.Fatal(err) + } + res, err := s.Scan(context.Background(), plan) + if err != nil { + t.Fatal(err) + } + checkFixture(t, res, plan) + t.Logf("the real tools found the %d planted candidates", res.Count) +} + +func TestDedupeIsDeterministic(t *testing.T) { + a := recall.Candidate{Tool: recall.ToolOpengrep, RuleIDVersioned: "gitlab-sast-rules/x@1", CWE: "CWE-22", Path: "a.go", StartLine: 3} + b := recall.Candidate{Tool: recall.ToolGosec, RuleIDVersioned: "gosec/G304@2.29.0", CWE: "CWE-22", Path: "a.go", StartLine: 3} + c := recall.Candidate{Tool: recall.ToolOpengrep, RuleIDVersioned: "gitlab-sast-rules/y@1", CWE: "CWE-78", Path: "a.go", StartLine: 3} + for _, in := range [][]recall.Candidate{{a, b, c}, {c, b, a}, {b, c, a}} { + got := recall.Dedupe(in) + if len(got) != 2 || got[0].Tool != recall.ToolGosec || got[1].CWE != "CWE-78" { + t.Fatalf("Dedupe(%v) = %v", in, got) + } + } +} + +func sha256Hex(b []byte) string { + s := sha256.Sum256(b) + return hex.EncodeToString(s[:]) +} + +// TestTheReviewersEvasionsAreRecorded holds the fixes for the same-family +// review of 2026-10-03 (docs/reviews/lane-b-gate.md): each way a scanned tree +// kept code from a tool, or broke the scan, is now either scanned or recorded +// as incomplete coverage, never silent. It needs the real tools, like +// TestTheFixtureWithTheRealTools. +func TestTheReviewersEvasionsAreRecorded(t *testing.T) { + tools := realTools() + if goBin, err := exec.LookPath("go"); err == nil && tools.GoBin == "" { + tools.GoBin = filepath.Dir(goBin) + } + root := t.TempDir() + outside := t.TempDir() + write := func(base, rel, body string) { + p := filepath.Join(base, rel) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + cmd := "package main\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n)\n\nfunc main() { _ = exec.Command(\"sh\", \"-c\", os.Args[1]).Run() }\n" + write(root, "mod/go.mod", "module example.invalid/m\n\ngo 1.22\n") + write(root, "mod/a.go", cmd) + write(root, "mod/w.go", "//go:build windows\n\n"+cmd) // another platform + write(root, "mod/c.go", "package main\n\n// #include \"/etc/hostname\"\nimport \"C\"\n\nfunc c() {}\n") // cgo + write(root, "mod/vendor/v/v.go", "package v\n") + write(root, "loose/l.go", cmd) // in no module + write(outside, "linked.go", cmd) + if err := os.Symlink(filepath.Join(outside, "linked.go"), filepath.Join(root, "mod", "linked.go")); err != nil { + t.Fatal(err) + } + write(root, ".github/scripts/rel.py", "import os\nos.system(input())\n") // bandit's default excludes + write(root, "CVSS-tool/app.py", "import os\nos.system(input())\n") + + s := recall.Scanner{Pack: pack(t), Tools: tools} + plan, err := s.Prepare(context.Background(), root) + if errors.Is(err, recall.ErrToolAbsent) { + if os.Getenv("ANVIL_LANEB_E2E") == "1" { + t.Fatalf("ANVIL_LANEB_E2E=1 and a recall tool is absent: %v", err) + } + t.Logf("a recall tool is not installed, and the scan refused as a missing tool: %v", err) + return + } + if err != nil { + t.Fatal(err) + } + res, err := s.Scan(context.Background(), plan) + if err != nil { + t.Fatalf("a symbolic link in a module broke the scan: %v", err) + } + problems := strings.Join(res.Problems, "\n") + for _, want := range []string{ + "gosec analysed", // w.go and c.go were not analysed + "belong to no Go module", // loose/l.go + "which is not a regular file", // the link gosec followed + } { + if !strings.Contains(problems, want) { + t.Errorf("no problem mentions %q; problems:\n%s", want, problems) + } + } + if !strings.Contains(strings.Join(res.Notes, "\n"), "under vendor/") { + t.Errorf("the vendored Go file is not noted: %v", res.Notes) + } + host, _ := os.ReadFile("/etc/hostname") + if h := strings.TrimSpace(string(host)); h != "" && strings.Contains(problems, h) { + t.Errorf("a cgo #include put a host file's contents into the scan's output") + } + byPath := map[string]bool{} + for _, c := range res.Candidates { + byPath[c.Tool+" "+c.Path] = true + } + for _, want := range []string{"bandit .github/scripts/rel.py", "bandit CVSS-tool/app.py"} { + if !byPath[want] { + t.Errorf("no candidate %q: bandit's default excludes are still in force (%v)", want, byPath) + } + } +} diff --git a/internal/recall/recalltest/doc.go b/internal/recall/recalltest/doc.go new file mode 100644 index 0000000..912ba1e --- /dev/null +++ b/internal/recall/recalltest/doc.go @@ -0,0 +1,5 @@ +// Package recalltest replays the three recall tools' real reports, recorded +// from testdata/laneb-fixture, so the Lane B chain can be tested where the +// tools are not installed (CI's Go job). Tests only: nothing in a shipped +// binary imports it. +package recalltest diff --git a/internal/recall/recalltest/recorded/bandit.json b/internal/recall/recalltest/recorded/bandit.json new file mode 100644 index 0000000..083c891 --- /dev/null +++ b/internal/recall/recalltest/recorded/bandit.json @@ -0,0 +1,140 @@ +{ + "errors": [], + "generated_at": "2026-10-04T03:52:04Z", + "metrics": { + "@LANEB_FIXTURE_ROOT@/src/app.py": { + "CONFIDENCE.HIGH": 3, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 1, + "SEVERITY.LOW": 1, + "SEVERITY.MEDIUM": 1, + "SEVERITY.UNDEFINED": 0, + "loc": 7, + "nosec": 0, + "skipped_tests": 0 + }, + "@LANEB_FIXTURE_ROOT@/src/clean.py": { + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 3, + "nosec": 0, + "skipped_tests": 0 + }, + "@LANEB_FIXTURE_ROOT@/src/suppressed.py": { + "CONFIDENCE.HIGH": 1, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 1, + "SEVERITY.UNDEFINED": 0, + "loc": 4, + "nosec": 0, + "skipped_tests": 0 + }, + "_totals": { + "CONFIDENCE.HIGH": 4, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 1, + "SEVERITY.LOW": 1, + "SEVERITY.MEDIUM": 2, + "SEVERITY.UNDEFINED": 0, + "loc": 14, + "nosec": 0, + "skipped_tests": 0 + } + }, + "results": [ + { + "code": "1 \"\"\"Planted for Lane B's fixture. Never run.\"\"\"\n2 import subprocess\n3 import sys\n", + "col_offset": 0, + "end_col_offset": 17, + "filename": "@LANEB_FIXTURE_ROOT@/src/app.py", + "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, + "issue_severity": "LOW", + "issue_text": "Consider possible security implications associated with the subprocess module.", + "line_number": 2, + "line_range": [ + 2 + ], + "more_info": "https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess", + "test_id": "B404", + "test_name": "blacklist" + }, + { + "code": "6 def run_expression():\n7 return eval(sys.argv[1])\n8 \n", + "col_offset": 11, + "end_col_offset": 28, + "filename": "@LANEB_FIXTURE_ROOT@/src/app.py", + "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, + "issue_severity": "MEDIUM", + "issue_text": "Use of possibly insecure function - consider using safer ast.literal_eval.", + "line_number": 7, + "line_range": [ + 7 + ], + "more_info": "https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_calls.html#b307-eval", + "test_id": "B307", + "test_name": "blacklist" + }, + { + "code": "10 def run_command(cmd):\n11 return subprocess.call(cmd, shell=True)\n", + "col_offset": 11, + "end_col_offset": 43, + "filename": "@LANEB_FIXTURE_ROOT@/src/app.py", + "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, + "issue_severity": "HIGH", + "issue_text": "subprocess call with shell=True identified, security issue.", + "line_number": 11, + "line_range": [ + 11 + ], + "more_info": "https://bandit.readthedocs.io/en/1.9.4/plugins/b602_subprocess_popen_with_shell_equals_true.html", + "test_id": "B602", + "test_name": "subprocess_popen_with_shell_equals_true" + }, + { + "code": "5 def run_expression():\n6 return eval(sys.argv[1]) # nosec # nosemgrep\n", + "col_offset": 11, + "end_col_offset": 28, + "filename": "@LANEB_FIXTURE_ROOT@/src/suppressed.py", + "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, + "issue_severity": "MEDIUM", + "issue_text": "Use of possibly insecure function - consider using safer ast.literal_eval.", + "line_number": 6, + "line_range": [ + 6 + ], + "more_info": "https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_calls.html#b307-eval", + "test_id": "B307", + "test_name": "blacklist" + } + ] +} \ No newline at end of file diff --git a/internal/recall/recalltest/recorded/bin/bandit b/internal/recall/recalltest/recorded/bin/bandit new file mode 100755 index 0000000..8bc4a73 --- /dev/null +++ b/internal/recall/recalltest/recorded/bin/bandit @@ -0,0 +1,4 @@ +#!/bin/sh +# A stand-in that recall resolves; recalltest.Replay answers its calls. Running it is an error. +echo "recalltest stand-in: not a real bandit" >&2 +exit 2 diff --git a/internal/recall/recalltest/recorded/bin/gosec b/internal/recall/recalltest/recorded/bin/gosec new file mode 100755 index 0000000..bb7028a --- /dev/null +++ b/internal/recall/recalltest/recorded/bin/gosec @@ -0,0 +1,4 @@ +#!/bin/sh +# A stand-in that recall resolves; recalltest.Replay answers its calls. Running it is an error. +echo "recalltest stand-in: not a real gosec" >&2 +exit 2 diff --git a/internal/recall/recalltest/recorded/bin/opengrep b/internal/recall/recalltest/recorded/bin/opengrep new file mode 100755 index 0000000..dc09e05 --- /dev/null +++ b/internal/recall/recalltest/recorded/bin/opengrep @@ -0,0 +1,4 @@ +#!/bin/sh +# A stand-in that recall resolves; recalltest.Replay answers its calls. Running it is an error. +echo "recalltest stand-in: not a real opengrep" >&2 +exit 2 diff --git a/internal/recall/recalltest/recorded/gosec.json b/internal/recall/recalltest/recorded/gosec.json new file mode 100644 index 0000000..b003485 --- /dev/null +++ b/internal/recall/recalltest/recorded/gosec.json @@ -0,0 +1,44 @@ +{ + "Golang errors": {}, + "Issues": [ + { + "severity": "HIGH", + "confidence": "HIGH", + "cwe": { + "id": "78", + "url": "https://cwe.mitre.org/data/definitions/78.html" + }, + "rule_id": "G702", + "details": "Command injection via taint analysis", + "file": "@LANEB_FIXTURE_ROOT@/src/main.go", + "code": "11: func (runner) run(name string) error {\n12: \treturn exec.Command(\"sh\", \"-c\", name).Run()\n13: }\n", + "line": "12", + "column": "21", + "nosec": false, + "suppressions": null + }, + { + "severity": "MEDIUM", + "confidence": "HIGH", + "cwe": { + "id": "78", + "url": "https://cwe.mitre.org/data/definitions/78.html" + }, + "rule_id": "G204", + "details": "Subprocess launched with variable", + "file": "@LANEB_FIXTURE_ROOT@/src/main.go", + "code": "11: func (runner) run(name string) error {\n12: \treturn exec.Command(\"sh\", \"-c\", name).Run()\n13: }\n", + "line": "12", + "column": "9", + "nosec": false, + "suppressions": null + } + ], + "Stats": { + "files": 1, + "lines": 17, + "nosec": 0, + "found": 2 + }, + "GosecVersion": "2.29.0" +} \ No newline at end of file diff --git a/internal/recall/recalltest/recorded/opengrep.json b/internal/recall/recalltest/recorded/opengrep.json new file mode 100644 index 0000000..d6c6b72 --- /dev/null +++ b/internal/recall/recalltest/recorded/opengrep.json @@ -0,0 +1 @@ +{"version":"1.26.0","results":[{"check_id":"java_crypto_rule-CipherDESInsecure","path":"@LANEB_FIXTURE_ROOT@/src/Digest.java","start":{"line":6,"col":16,"offset":154},"end":{"line":6,"col":58,"offset":196},"extra":{"metavars":{"$1":{"start":{"line":1,"col":4,"offset":3},"end":{"line":1,"col":5,"offset":4},"abstract_content":"/"},"$ALG":{"start":{"line":6,"col":36,"offset":174},"end":{"line":6,"col":56,"offset":194},"abstract_content":"DES/CBC/PKCS5Padding"}},"message":"DES, TripleDES and RC2 are all considered broken or insecure cryptographic algorithms.\nNewer algorithms apply message integrity to validate ciphertext has not been tampered\nwith. Consider using `ChaCha20Poly1305` instead as it is easier and faster than the\nalternatives such as `AES-256-GCM`.\n\nFor older applications that don't have support for `ChaCha20Poly1305`,\n`AES-256-GCM` is recommended, however it has many drawbacks:\n- Slower than `ChaCha20Poly1305`.\n- Catastrophic failure if nonce values are reused.\n\nExample using `ChaCha20Poly1305`:\n```\npublic encrypt() throws Exception {\n chaChaEncryption(\"Secret text to encrypt\".getBytes(StandardCharsets.UTF_8));\n}\n\npublic SecureRandom getSecureRandomDRBG() throws NoSuchAlgorithmException {\n // Use DRBG according to\nhttp://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf\n return SecureRandom.getInstance(\"DRBG\",\n // Security strength in bits (default is 128)\n DrbgParameters.instantiation(256,\n // Set prediction resistance and re-seeding\n DrbgParameters.Capability.PR_AND_RESEED,\n // Set the personalization string (optional, not necessary)\n \"some_personalization_string\".getBytes()\n )\n );\n}\n\npublic Cipher getChaCha20Poly1305(int mode, byte[] ivKey, byte[] secretKey) throws\nNoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,\nInvalidAlgorithmParameterException {\n // Get a DRBG random number generator instance\n SecureRandom random = getSecureRandomDRBG();\n // Create a ChaCha20-Poly1305 cipher instance\n Cipher chaChaCipher = Cipher.getInstance(\"ChaCha20-Poly1305/None/NoPadding\");\n // Create our parameterSpec using our ivKey\n AlgorithmParameterSpec parameterSpec = new IvParameterSpec(ivKey);\n // Create a SecretKeySpec using our secretKey\n SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, \"ChaCha20\");\n // Initialize and return the cipher for the provided mode\n chaChaCipher.init(mode, secretKeySpec, parameterSpec, random);\n return chaChaCipher;\n}\n\npublic void chaChaEncryption(byte[] plainText) throws NoSuchAlgorithmException,\nNoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {\n // Get a DRBG random number generator instance\n SecureRandom random = getSecureRandomDRBG();\n // Create secretKey\n byte[] secretKey = new byte[32];\n random.nextBytes(secretKey);\n // Create an IV Key\n byte[] ivKey = new byte[12];\n random.nextBytes(ivKey);\n\n // Create a chaCha encryption cipher instance\n Cipher chaChaEncryptor = getChaCha20Poly1305(Cipher.ENCRYPT_MODE, ivKey, secretKey);\n\n // Encrypt the text using ChaCha20Poly1305\n byte[] cipherText = null;\n try {\n cipherText = chaChaEncryptor.doFinal(plainText);\n } catch (IllegalBlockSizeException | BadPaddingException e) {\n System.out.println(\"failed to encrypt text\");\n return;\n }\n System.out.println(\"encrypted: \" + Base64.getEncoder().encodeToString(cipherText));\n\n // Create a chaCha decryption cipher instance\n Cipher chaChaDecryptor = getChaCha20Poly1305(Cipher.DECRYPT_MODE, ivKey, secretKey);\n\n // Decrypt the text\n byte[] decryptedText = null;\n try {\n decryptedText = chaChaDecryptor.doFinal(cipherText);\n } catch (IllegalBlockSizeException | BadPaddingException e) {\n System.out.println(\"failed to decrypt text\");\n return;\n }\n System.out.println(\"decrypted: \" + new String(decryptedText, StandardCharsets.UTF_8));\n}\n```\n\nFor more information on Java Cryptography see:\nhttps://docs.oracle.com/en/java/javase/15/security/java-cryptography-architecture-jca-reference-guide.html\n","metadata":{"shortDescription":"Inadequate encryption strength","category":"security","cwe":"CWE-326","owasp":["A3:2017-Sensitive Data Exposure","A02:2021-Cryptographic Failures","A04:2025-Cryptographic Failures"],"technology":["java"],"security-severity":"Medium"},"severity":"WARNING","fingerprint":"61c22c0d187c822d5dab8dcb1343f1fbed6737bac3c3c58fd2fee043b1783592e9139ef22178314a672f501ffa30da5a76a224829bcb90455fbe299ecc91ec2b_0","lines":" return Cipher.getInstance(\"DES/CBC/PKCS5Padding\");","is_ignored":false,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"cipher","start":{"line":5,"col":5,"offset":104},"end":{"line":7,"col":6,"offset":203}},{"kind":"class","name":"Digest","start":{"line":4,"col":1,"offset":78},"end":{"line":8,"col":2,"offset":205}}]}},{"check_id":"java_crypto_rule-CipherIntegrity","path":"@LANEB_FIXTURE_ROOT@/src/Digest.java","start":{"line":6,"col":16,"offset":154},"end":{"line":6,"col":58,"offset":196},"extra":{"metavars":{"$ALG":{"start":{"line":6,"col":36,"offset":174},"end":{"line":6,"col":56,"offset":194},"abstract_content":"DES/CBC/PKCS5Padding"}},"message":"Cryptographic algorithms provide many different modes of operation, only some of which provide\nmessage integrity. Without message integrity it could be possible for an adversary to attempt\nto tamper with the ciphertext which could lead to compromising the encryption key. Newer\nalgorithms\napply message integrity to validate ciphertext has not been tampered with.\n\nInstead of using an algorithm that requires configuring a cipher mode, an algorithm\nthat has built-in message integrity should be used. Consider using `ChaCha20Poly1305` or\n`AES-256-GCM` instead.\n\nFor older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is\nrecommended, however it has many drawbacks:\n - Slower than `ChaCha20Poly1305`.\n - Catastrophic failure if nonce values are reused.\n\nExample using `ChaCha20Poly1305`:\n```\npublic encrypt() throws Exception {\n chaChaEncryption(\"Secret text to encrypt\".getBytes(StandardCharsets.UTF_8));\n}\n\npublic SecureRandom getSecureRandomDRBG() throws NoSuchAlgorithmException {\n// Use DRBG according to\nhttp://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf\n return SecureRandom.getInstance(\"DRBG\",\n // Security strength in bits (default is 128)\n DrbgParameters.instantiation(256,\n // Set prediction resistance and re-seeding\n DrbgParameters.Capability.PR_AND_RESEED,\n // Set the personalization string (optional, not necessary)\n \"some_personalization_string\".getBytes()\n )\n );\n}\n\npublic Cipher getChaCha20Poly1305(int mode, byte[] ivKey, byte[] secretKey) throws\nNoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,\nInvalidAlgorithmParameterException {\n // Get a DRBG random number generator instance\n SecureRandom random = getSecureRandomDRBG();\n // Create a ChaCha20-Poly1305 cipher instance\n Cipher chaChaCipher = Cipher.getInstance(\"ChaCha20-Poly1305/None/NoPadding\");\n // Create our parameterSpec using our ivKey\n AlgorithmParameterSpec parameterSpec = new IvParameterSpec(ivKey);\n // Create a SecretKeySpec using our secretKey\n SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, \"ChaCha20\");\n // Initialize and return the cipher for the provided mode\n chaChaCipher.init(mode, secretKeySpec, parameterSpec, random);\n return chaChaCipher;\n}\n\npublic void chaChaEncryption(byte[] plainText) throws NoSuchAlgorithmException,\nNoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {\n // Get a DRBG random number generator instance\n SecureRandom random = getSecureRandomDRBG();\n // Create secretKey\n byte[] secretKey = new byte[32];\n random.nextBytes(secretKey);\n // Create an IV Key\n byte[] ivKey = new byte[12];\n random.nextBytes(ivKey);\n\n // Create a chaCha encryption cipher instance\n Cipher chaChaEncryptor = getChaCha20Poly1305(Cipher.ENCRYPT_MODE, ivKey, secretKey);\n\n // Encrypt the text using ChaCha20Poly1305\n byte[] cipherText = null;\n try {\n cipherText = chaChaEncryptor.doFinal(plainText);\n } catch (IllegalBlockSizeException | BadPaddingException e) {\n System.out.println(\"failed to encrypt text\");\n return;\n }\n System.out.println(\"encrypted: \" + Base64.getEncoder().encodeToString(cipherText));\n\n // Create a chaCha decryption cipher instance\n Cipher chaChaDecryptor = getChaCha20Poly1305(Cipher.DECRYPT_MODE, ivKey, secretKey);\n\n // Decrypt the text\n byte[] decryptedText = null;\n try {\n decryptedText = chaChaDecryptor.doFinal(cipherText);\n } catch (IllegalBlockSizeException | BadPaddingException e) {\n System.out.println(\"failed to decrypt text\");\n return;\n }\n System.out.println(\"decrypted: \" + new String(decryptedText, StandardCharsets.UTF_8));\n}\n```\n\nFor more information on Java Cryptography see:\nhttps://docs.oracle.com/en/java/javase/15/security/java-cryptography-architecture-jca-reference-guide.html\n","metadata":{"shortDescription":"Use of a broken or risky cryptographic algorithm","category":"security","cwe":"CWE-327","owasp":["A3:2017-Sensitive Data Exposure","A02:2021-Cryptographic Failures","A04:2025-Cryptographic Failures"],"technology":["java"],"security-severity":"Medium"},"severity":"ERROR","fingerprint":"dc93357c925427e45baef28837871d367963414c14cacf7590c529ffa1109dd75b01ecb41bb5addb7c6a50b0caaa1bfa5019de6da09d0c4ba1e86e7e001f57c2_0","lines":" return Cipher.getInstance(\"DES/CBC/PKCS5Padding\");","is_ignored":false,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"cipher","start":{"line":5,"col":5,"offset":104},"end":{"line":7,"col":6,"offset":203}},{"kind":"class","name":"Digest","start":{"line":4,"col":1,"offset":78},"end":{"line":8,"col":2,"offset":205}}]}},{"check_id":"java_crypto_rule-CipherPaddingOracle","path":"@LANEB_FIXTURE_ROOT@/src/Digest.java","start":{"line":6,"col":16,"offset":154},"end":{"line":6,"col":58,"offset":196},"extra":{"metavars":{"$ALG":{"start":{"line":6,"col":36,"offset":174},"end":{"line":6,"col":56,"offset":194},"abstract_content":"DES/CBC/PKCS5Padding"}},"message":"Cryptographic block ciphers can be configured to pad individual blocks if there is not enough\ninput data to match the size of the block. This specific mode of CBC used in combination with\nPKCS5Padding is susceptible to padding oracle attacks. An adversary could potentially decrypt\nthe message if the system exposed the difference between plaintext with invalid padding or\nvalid padding. The distinction between valid and invalid padding is usually revealed through\ndistinct error messages being returned for each condition.\n\nConsider switching to a more secure cipher that doesn't require padding and builds in message\nauthentication integrity directly into the algorithm.\n\nConsider using `ChaCha20Poly1305` or\n`AES-256-GCM` instead.\n\nFor older applications that don't have support for `ChaCha20Poly1305`, `AES-256-GCM` is\nrecommended, however it has many drawbacks:\n - Slower than `ChaCha20Poly1305`.\n - Catastrophic failure if nonce values are reused.\n\nExample using `ChaCha20Poly1305`:\n```\npublic encrypt() throws Exception {\n chaChaEncryption(\"Secret text to encrypt\".getBytes(StandardCharsets.UTF_8));\n}\n\npublic SecureRandom getSecureRandomDRBG() throws NoSuchAlgorithmException {\n // Use DRBG according to\nhttp://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf\n return SecureRandom.getInstance(\"DRBG\",\n // Security strength in bits (default is 128)\n DrbgParameters.instantiation(256,\n // Set prediction resistance and re-seeding\n DrbgParameters.Capability.PR_AND_RESEED,\n // Set the personalization string (optional, not necessary)\n \"some_personalization_string\".getBytes()\n )\n );\n}\n\npublic Cipher getChaCha20Poly1305(int mode, byte[] ivKey, byte[] secretKey) throws\nNoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,\nInvalidAlgorithmParameterException {\n // Get a DRBG random number generator instance\n SecureRandom random = getSecureRandomDRBG();\n // Create a ChaCha20-Poly1305 cipher instance\n Cipher chaChaCipher = Cipher.getInstance(\"ChaCha20-Poly1305/None/NoPadding\");\n // Create our parameterSpec using our ivKey\n AlgorithmParameterSpec parameterSpec = new IvParameterSpec(ivKey);\n // Create a SecretKeySpec using our secretKey\n SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, \"ChaCha20\");\n // Initialize and return the cipher for the provided mode\n chaChaCipher.init(mode, secretKeySpec, parameterSpec, random);\n return chaChaCipher;\n}\n\npublic void chaChaEncryption(byte[] plainText) throws NoSuchAlgorithmException,\nNoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {\n // Get a DRBG random number generator instance\n SecureRandom random = getSecureRandomDRBG();\n // Create secretKey\n byte[] secretKey = new byte[32];\n random.nextBytes(secretKey);\n // Create an IV Key\n byte[] ivKey = new byte[12];\n random.nextBytes(ivKey);\n\n // Create a chaCha encryption cipher instance\n Cipher chaChaEncryptor = getChaCha20Poly1305(Cipher.ENCRYPT_MODE, ivKey, secretKey);\n\n // Encrypt the text using ChaCha20Poly1305\n byte[] cipherText = null;\n try {\n cipherText = chaChaEncryptor.doFinal(plainText);\n } catch (IllegalBlockSizeException | BadPaddingException e) {\n System.out.println(\"failed to encrypt text\");\n return;\n }\n System.out.println(\"encrypted: \" + Base64.getEncoder().encodeToString(cipherText));\n\n // Create a chaCha decryption cipher instance\n Cipher chaChaDecryptor = getChaCha20Poly1305(Cipher.DECRYPT_MODE, ivKey, secretKey);\n\n // Decrypt the text\n byte[] decryptedText = null;\n try {\n decryptedText = chaChaDecryptor.doFinal(cipherText);\n } catch (IllegalBlockSizeException | BadPaddingException e) {\n System.out.println(\"failed to decrypt text\");\n return;\n }\n System.out.println(\"decrypted: \" + new String(decryptedText, StandardCharsets.UTF_8));\n}\n```\n\nFor more information on padding oracle attacks see:\nhttps://en.wikipedia.org/wiki/Padding_oracle_attack\n\nFor more information on Java Cryptography see:\nhttps://docs.oracle.com/en/java/javase/15/security/java-cryptography-architecture-jca-reference-guide.html\n","metadata":{"shortDescription":"Use of a broken or risky cryptographic algorithm","category":"security","cwe":"CWE-327","owasp":["A3:2017-Sensitive Data Exposure","A02:2021-Cryptographic Failures","A04:2025-Cryptographic Failures"],"technology":["java"],"security-severity":"Medium"},"severity":"ERROR","fingerprint":"816e1595d6d563641259c1ac09ff577e591607a19abccec76ea7a05521b61dfdf7f4accb9266a6176a5f3fe0e4c97c179a76788e2455165d17a8ea607fab6861_0","lines":" return Cipher.getInstance(\"DES/CBC/PKCS5Padding\");","is_ignored":false,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"cipher","start":{"line":5,"col":5,"offset":104},"end":{"line":7,"col":6,"offset":203}},{"kind":"class","name":"Digest","start":{"line":4,"col":1,"offset":78},"end":{"line":8,"col":2,"offset":205}}]}},{"check_id":"python_eval_rule-eval","path":"@LANEB_FIXTURE_ROOT@/src/app.py","start":{"line":7,"col":12,"offset":111},"end":{"line":7,"col":29,"offset":128},"extra":{"metavars":{"$X":{"start":{"line":7,"col":17,"offset":116},"end":{"line":7,"col":28,"offset":127},"abstract_content":"sys.argv[1]"}},"message":"The application was found calling the `eval` function with non-literal data. If the variable contains \nuser-controlled data, either partially or fully, an adversary could compromise the entire system by \nexecuting arbitrary Python code.\n\nTo remediate this issue, remove all calls to `eval` and consider alternative methods for executing \nthe necessary business logic. There is almost no safe method of calling `eval` with user-supplied input.\n\nIf the application only needs to convert strings into objects, consider using `json.loads`. In \nsome cases `ast.literal_eval` is recommended, but this should be avoided as it can still suffer \nfrom other issues such as the ability for malicious code to crash the python interpreter or application.\n\nExample using `json.loads`` to load in arbitrary data to create data structures:\n```\n# User supplied data as a blob of JSON\nuser_supplied_data = \"\"\"{\"user\": \"test\", \"metadata\": [1,2,3]}\"\"\"\n# Load the JSON\nuser_object = json.loads(user_supplied_data)\n# Manually add protected properties _after_ loading, never before\nuser_object[\"is_admin\"] = False\n# Work with the object\n```\n","metadata":{"cwe":"CWE-95","owasp":["A1:2017-Injection","A03:2021-Injection","A05:2025-Injection"],"category":"security","shortDescription":"Improper neutralization of directives in dynamically evaluated code ('Eval Injection')","security-severity":"High"},"severity":"WARNING","fingerprint":"652a4b03a8dae10880ad15c530ce931d856a9ca659cffbc233924d254121c8cb0799e8f9193be8809368394b66c11b78cbf3318e35dfd198f837076ed20d16a0_0","lines":" return eval(sys.argv[1])","is_ignored":false,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"run_expression","start":{"line":6,"col":1,"offset":78},"end":{"line":7,"col":29,"offset":128}}]}},{"check_id":"python_exec_rule-subprocess-popen-shell-true","path":"@LANEB_FIXTURE_ROOT@/src/app.py","start":{"line":11,"col":12,"offset":164},"end":{"line":11,"col":44,"offset":196},"extra":{"metavars":{"$FUNC":{"start":{"line":11,"col":23,"offset":175},"end":{"line":11,"col":27,"offset":179},"abstract_content":"call"},"$ARG":{"start":{"line":11,"col":28,"offset":180},"end":{"line":11,"col":31,"offset":183},"abstract_content":"cmd"}},"message":"Found `subprocess` function `call` with `shell=True`. This is dangerous because this call will\nspawn the command using a shell process. Doing so propagates current shell settings and\nvariables,\nwhich makes it much easier for a malicious actor to execute commands. Use `shell=False`\ninstead.\n","metadata":{"cwe":"CWE-78","owasp":["A1:2017-Injection","A03:2021-Injection","A05:2025-Injection"],"shortDescription":"Improper neutralization of special elements used in an OS Command ('OS Command Injection')","security-severity":"High","category":"security"},"severity":"ERROR","fingerprint":"a7b1ce9e766d97a27ef66571e364dff94c442321e24bf737f979993e567724ff83154f57e4f3ed1f0ee2dda68538b2013589cafe1ff3b82ae14b21569eeeed34_0","lines":" return subprocess.call(cmd, shell=True)","is_ignored":false,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"run_command","start":{"line":10,"col":1,"offset":131},"end":{"line":11,"col":44,"offset":196}}]}},{"check_id":"raptor-insecure-api-strcpy-strcat","path":"@LANEB_FIXTURE_ROOT@/src/copy.c","start":{"line":6,"col":5,"offset":164},"end":{"line":6,"col":21,"offset":180},"extra":{"metavars":{},"message":"A buffer overflow condition exists when a program attempts to put more data in a buffer than it can hold, or when a program attempts to put data in a memory area outside of the boundaries of a buffer.","metadata":{"author":"Marco Ivaldi ","category":"security","subcategory":["vuln"],"vulnerability_class":"Dangerous Method or Function","likelihood":"HIGH","impact":"HIGH","confidence":"HIGH","technology":["c","cpp"],"cwe":["CWE-676: Use of Potentially Dangerous Function","CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')","CWE-787: Out-of-bounds Write"],"owasp":["A10:2025 - Mishandling of Exceptional Conditions"],"references":["https://g.co/kgs/PCHQjJ"]},"severity":"ERROR","fingerprint":"4543be4c58062a6e2a3e01412d8976f0b266ecb2f987db9684bc0032bc7429db6b07b46ad71c406e95133612ff6be51d1ee6229e613cc89200fd778d21bc52a2_0","lines":" strcpy(buf, src);","is_ignored":false,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"copy","start":{"line":4,"col":1,"offset":113},"end":{"line":7,"col":2,"offset":183}}]}},{"check_id":"raptor-write-into-stack-buffer","path":"@LANEB_FIXTURE_ROOT@/src/copy.c","start":{"line":6,"col":5,"offset":164},"end":{"line":6,"col":21,"offset":180},"extra":{"metavars":{"$FUN":{"start":{"line":6,"col":5,"offset":164},"end":{"line":6,"col":11,"offset":170},"abstract_content":"strcpy"},"$BUF":{"start":{"line":6,"col":12,"offset":171},"end":{"line":6,"col":15,"offset":174},"abstract_content":"buf"}},"message":"The software directly writes into a stack buffer. This might lead to a stack-based buffer overflow.","metadata":{"author":"Marco Ivaldi ","category":"security","subcategory":["audit"],"vulnerability_class":"Memory Issues","likelihood":"LOW","impact":"LOW","confidence":"MEDIUM","technology":["c","cpp"],"cwe":["CWE-121: Stack-based Buffer Overflow"],"owasp":["A08:2025 - Software or Data Integrity Failures"],"references":["https://github.com/googleprojectzero/weggli"]},"severity":"INFO","fingerprint":"05f26f08d0800c8b83403069a48c3e3e960f326056744687c60167f2a99ff330e5c58f061001fc5daf0ef14590aece5ec8e59229593ca70382cc16b1c86dfeef_0","lines":" strcpy(buf, src);","is_ignored":false,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"copy","start":{"line":4,"col":1,"offset":113},"end":{"line":7,"col":2,"offset":183}}]}},{"check_id":"python_eval_rule-eval","path":"@LANEB_FIXTURE_ROOT@/src/suppressed.py","start":{"line":6,"col":12,"offset":130},"end":{"line":6,"col":29,"offset":147},"extra":{"metavars":{"$X":{"start":{"line":6,"col":17,"offset":135},"end":{"line":6,"col":28,"offset":146},"abstract_content":"sys.argv[1]"}},"message":"The application was found calling the `eval` function with non-literal data. If the variable contains \nuser-controlled data, either partially or fully, an adversary could compromise the entire system by \nexecuting arbitrary Python code.\n\nTo remediate this issue, remove all calls to `eval` and consider alternative methods for executing \nthe necessary business logic. There is almost no safe method of calling `eval` with user-supplied input.\n\nIf the application only needs to convert strings into objects, consider using `json.loads`. In \nsome cases `ast.literal_eval` is recommended, but this should be avoided as it can still suffer \nfrom other issues such as the ability for malicious code to crash the python interpreter or application.\n\nExample using `json.loads`` to load in arbitrary data to create data structures:\n```\n# User supplied data as a blob of JSON\nuser_supplied_data = \"\"\"{\"user\": \"test\", \"metadata\": [1,2,3]}\"\"\"\n# Load the JSON\nuser_object = json.loads(user_supplied_data)\n# Manually add protected properties _after_ loading, never before\nuser_object[\"is_admin\"] = False\n# Work with the object\n```\n","metadata":{"cwe":"CWE-95","owasp":["A1:2017-Injection","A03:2021-Injection","A05:2025-Injection"],"category":"security","shortDescription":"Improper neutralization of directives in dynamically evaluated code ('Eval Injection')","security-severity":"High"},"severity":"WARNING","fingerprint":"5d930249ac1114d3ed3b1e76e1310d9912d0c70d99291def65fb588bc8696fc73e649a9467b92bb6965551b40331f4c76165c9a79476da953e2f202462139232_0","lines":" return eval(sys.argv[1]) # nosec # nosemgrep","is_ignored":true,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"run_expression","start":{"line":5,"col":1,"offset":97},"end":{"line":6,"col":29,"offset":147}}]}},{"check_id":"javascript_require_rule-non-literal-require","path":"@LANEB_FIXTURE_ROOT@/web/app.js","start":{"line":3,"col":10,"offset":75},"end":{"line":3,"col":23,"offset":88},"extra":{"metavars":{"$OBJ":{"start":{"line":3,"col":18,"offset":83},"end":{"line":3,"col":22,"offset":87},"abstract_content":"name"}},"message":"The application was found to dynamically import a module by calling `require` using a\nnon-literal string. An adversary might be able to read the first line of\narbitrary files. If they had write access to the file system, they may also be able to\nexecute arbitrary code.\n\nTo remediate this issue, use a hardcoded string literal when calling `require`. Never call it\nit with dynamically created variables or user-supplied data.\n","metadata":{"cwe":"CWE-95","owasp":["A1:2017-Injection","A03:2021-Injection","A05:2025-Injection"],"shortDescription":"Improper neutralization of directives in dynamically evaluated code ('Eval Injection')","category":"security","source-rule-url":"https://github.com/nodesecurity/eslint-plugin-security/blob/master/rules/detect-non-literal-require.js","security-severity":"Low"},"severity":"WARNING","fingerprint":"6e13f9871f9956b4c5623a986ba9acfc44d9e2eaeac8681d2a730d2c40f3ebf1f2ea61df802f6fa4e0ffa4ed217335dd42bd66dcc6a4aac8e26fdc80ff6d0382_0","lines":" return require(name);","is_ignored":false,"validation_state":"NO_VALIDATOR","engine_kind":"OSS","enclosing_context":[{"kind":"function","name":"load","start":{"line":2,"col":1,"offset":44},"end":{"line":4,"col":2,"offset":91}}]}}],"errors":[],"paths":{"scanned":["@LANEB_FIXTURE_ROOT@/src/Digest.java","@LANEB_FIXTURE_ROOT@/src/app.py","@LANEB_FIXTURE_ROOT@/src/clean.py","@LANEB_FIXTURE_ROOT@/src/copy.c","@LANEB_FIXTURE_ROOT@/src/main.go","@LANEB_FIXTURE_ROOT@/src/suppressed.py","@LANEB_FIXTURE_ROOT@/web/app.js"]},"interfile_languages_used":[],"skipped_rules":[]} diff --git a/internal/recall/recalltest/replay.go b/internal/recall/recalltest/replay.go new file mode 100644 index 0000000..38483fc --- /dev/null +++ b/internal/recall/recalltest/replay.go @@ -0,0 +1,134 @@ +package recalltest + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/Susquehanna-Syntax/Anvil/internal/recall" +) + +// RootToken stands for the fixture's absolute path in a recorded report. +const RootToken = "@LANEB_FIXTURE_ROOT@" + +// Dir is the directory holding the recorded reports. +func Dir() string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "recorded") +} + +// FixtureRoot is testdata/laneb-fixture in this checkout. +func FixtureRoot() string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "..", "..", "..", "testdata", "laneb-fixture") +} + +// RulePack is data/rules in this checkout. +func RulePack() string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "..", "..", "..", "data", "rules") +} + +// Replay is an Exec that answers each tool with its recorded report, with the +// fixture root put back in. Version commands answer with the pinned versions. +// Tools names the binaries it answers to, so a test points recall.Tools at +// them; they need not exist on disk. +type Replay struct { + Root string + // Calls records every invocation, for tests that assert on arguments. + mu sync.Mutex + Calls [][]string +} + +// Tools are the binary paths Replay answers to. They are absolute paths into +// the recorded directory, which hold small executable stand-ins so that +// recall's resolve step, which stats the file, accepts them. +func Tools() recall.Tools { + d := Dir() + return recall.Tools{ + Opengrep: filepath.Join(d, "bin", recall.ToolOpengrep), + Gosec: filepath.Join(d, "bin", recall.ToolGosec), + Bandit: filepath.Join(d, "bin", recall.ToolBandit), + } +} + +// Run implements recall.Exec. +func (r *Replay) Run(_ context.Context, bin string, args []string, _ string, _ []string) ([]byte, []byte, int, error) { + r.mu.Lock() + r.Calls = append(r.Calls, append([]string{bin}, args...)) + r.mu.Unlock() + tool := filepath.Base(bin) + if len(args) == 1 && strings.Contains(args[0], "version") { + switch tool { + case recall.ToolOpengrep: + return []byte("1.26.0\n"), nil, 0, nil + case recall.ToolGosec: + return []byte("Version: 2.29.0\nGit tag: v2.29.0\n"), nil, 0, nil + case recall.ToolBandit: + return []byte("bandit 1.9.4\n"), nil, 0, nil + } + } + raw, err := os.ReadFile(filepath.Join(Dir(), tool+".json")) + if err != nil { + return nil, nil, 0, err + } + raw = bytes.ReplaceAll(raw, []byte(RootToken), []byte(r.Root)) + switch tool { + case recall.ToolOpengrep: + return raw, nil, 0, nil + case recall.ToolGosec: + for _, a := range args { + if out, ok := strings.CutPrefix(a, "-out="); ok { + return nil, nil, 0, os.WriteFile(out, raw, 0o600) + } + } + case recall.ToolBandit: + for i, a := range args { + if a == "-o" && i+1 < len(args) { + return nil, nil, 1, os.WriteFile(args[i+1], raw, 0o600) + } + } + } + return nil, nil, 0, errors.New("recalltest: no report destination in " + fmt.Sprint(args)) +} + +// Recorder wraps a real Exec and saves each tool's report with the fixture +// root replaced by RootToken. It is how the recorded reports were made. +type Recorder struct { + Root string + Real recall.Exec +} + +// Run implements recall.Exec. +func (r Recorder) Run(ctx context.Context, bin string, args []string, dir string, env []string) ([]byte, []byte, int, error) { + out, errb, code, err := r.Real.Run(ctx, bin, args, dir, env) + if err != nil || (len(args) == 1 && strings.Contains(args[0], "version")) { + return out, errb, code, err + } + tool := filepath.Base(bin) + for _, t := range []string{recall.ToolOpengrep, recall.ToolGosec, recall.ToolBandit} { + if strings.Contains(tool, t) { + tool = t + } + } + report := out + for i, a := range args { + if p, ok := strings.CutPrefix(a, "-out="); ok { + report, _ = os.ReadFile(p) + } + if a == "-o" && i+1 < len(args) { + report, _ = os.ReadFile(args[i+1]) + } + } + report = bytes.ReplaceAll(report, []byte(r.Root), []byte(RootToken)) + if werr := os.WriteFile(filepath.Join(Dir(), tool+".json"), report, 0o644); werr != nil { + return out, errb, code, werr + } + return out, errb, code, err +} diff --git a/internal/recall/rules.go b/internal/recall/rules.go new file mode 100644 index 0000000..6b87eb4 --- /dev/null +++ b/internal/recall/rules.go @@ -0,0 +1,461 @@ +// This file loads and verifies the vendored rule pack, data/rules: the owner's +// selection (selection.json), the generated manifest (MANIFEST.json), the +// rule files and the licence bodies. +// +// THE PACK IS VERIFIED ON EVERY LOAD, NOT TRUSTED. Every rule file and licence +// body must hash to the manifest, the manifest must name the selection it was +// generated from by digest, and no rule file may sit in a corpus directory +// unlisted: opengrep is only ever handed listed files, but an unlisted file is +// a selection nobody reviewed. A pack that fails any check is refused, and a +// refused pack is a missing tool, never an empty rule set. +// +// THE LICENCE GUARDS ARE ALLOWLISTS. A corpus is admitted only if its +// repository is in admittedCorpora, a GitLab directory only if it is in +// GitLabDirectoryAllowlist, and a rule only if its licence is in +// AdmittedLicences. GitLab's sast-rules LICENSE puts doc/ under CC BY-SA 4.0 +// and keeps third-party components under their own licences, and its c/ +// directory turned out to be one of them: every rule there is headed +// "License: GPL 2.0" (generated from flawfinder). The owner excluded it on +// 2026-10-03. Semgrep's own rules, the archived opengrep/opengrep-rules and +// lambdasec/autogrep are not in admittedCorpora, so none of them can be +// vendored by adding a line to selection.json. + +package recall + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path" + "path/filepath" + "slices" + "sort" + "strings" +) + +// ErrRulePack reports a rule pack that is absent, unreadable or fails +// verification. It is always a refusal: Lane B never runs a partial pack. +var ErrRulePack = errors.New("recall: the rule pack is missing or fails verification") + +// AdmittedLicences are the licences a vendored rule may carry. Both are +// permissive and flow into Apache-2.0. +var AdmittedLicences = []string{"MIT", "Apache-2.0"} + +// GitLabDirectoryAllowlist is every directory of GitLab's sast-rules Lane B +// may vendor from. c/ is absent on purpose (GPL-2.0 rule headers); doc/, ee/, +// jh/, rules/ (the lgpl and lgpl-cc trees), qa/, mappings/ and ci/ were never +// in it. +var GitLabDirectoryAllowlist = []string{"csharp", "go", "java", "javascript", "python", "scala"} + +// admittedCorpora maps each corpus name to the only repository it may come +// from, and the directories it may contribute. +var admittedCorpora = map[string]struct { + repository string + directories []string +}{ + "gitlab-sast-rules": {"https://gitlab.com/gitlab-org/security-products/sast-rules", GitLabDirectoryAllowlist}, + "0xdea-semgrep-rules": {"https://github.com/0xdea/semgrep-rules", []string{"rules/c"}}, +} + +// Selection is data/rules/selection.json. +type Selection struct { + Version int `json:"version"` + DecidedBy string `json:"decided_by"` + DecidedOn string `json:"decided_on"` + About string `json:"about"` + Tools []Tool `json:"tools"` + Corpora []Corpus `json:"corpora"` + ExcludedDirectories []struct { + Corpus string `json:"corpus"` + Directory string `json:"directory"` + Reason string `json:"reason"` + } `json:"excluded_directories"` + ExcludedRuleFiles []struct { + Corpus string `json:"corpus"` + Path string `json:"path"` + Reason string `json:"reason"` + } `json:"excluded_rule_files"` + ExcludeByDerivedLicence struct { + Licences []string `json:"licences"` + Decided string `json:"decided"` + Reason string `json:"reason"` + } `json:"exclude_by_derived_licence"` + ExcludedToolRules []struct { + Tool string `json:"tool"` + Rule string `json:"rule"` + Reason string `json:"reason"` + } `json:"excluded_tool_rules"` + ExcludedPaths struct { + Reason string `json:"reason"` + Directories []string `json:"directories"` + FileGlobs []string `json:"file_globs"` + } `json:"excluded_paths"` +} + +// Corpus is one pinned rule corpus in the selection. +type Corpus struct { + Name string `json:"name"` + Repository string `json:"repository"` + Commit string `json:"commit"` + LicenceFile string `json:"licence_file"` + RuleSuffix string `json:"rule_suffix"` + Directories []string `json:"directories"` +} + +// Rule is one vendored rule file, as the manifest records it. +type Rule struct { + Corpus string `json:"corpus"` + Repository string `json:"repository"` + Commit string `json:"commit"` + Path string `json:"path"` // relative to the pack root + BlobSHA1 string `json:"blob_sha1"` + SHA256 string `json:"sha256"` + IDs []string `json:"ids"` + CWEs []string `json:"cwes"` + Languages []string `json:"languages"` + Licence string `json:"licence"` + LicenceEvidence string `json:"licence_evidence"` + LicenceFile string `json:"licence_file"` +} + +// Manifest is data/rules/MANIFEST.json. +type Manifest struct { + Version int `json:"version"` + GeneratedBy string `json:"generated_by"` + SelectionSHA256 string `json:"selection_sha256"` + Licences []struct { + Path string `json:"path"` + SHA256 string `json:"sha256"` + } `json:"licences"` + Rules []Rule `json:"rules"` + // ExcludedByLicence lists every rule the generator left out because its + // GitLab companion names an upstream licence the selection excludes. + ExcludedByLicence []struct { + Path string `json:"path"` + Licence string `json:"licence"` + Evidence string `json:"evidence"` + } `json:"excluded_by_licence"` +} + +// Pack is a verified rule pack. +type Pack struct { + Root string + Selection Selection + Manifest Manifest + // SelectionSHA256 is the digest of selection.json, which the manifest + // names; it identifies the whole selection in scan_run.ruleset_version. + SelectionSHA256 string + + byID map[string]*Rule + excludedTools map[string]bool // "tool/rule" +} + +// Rules returns the vendored rules in manifest order. +func (p *Pack) Rules() []Rule { return p.Manifest.Rules } + +// RuleByID resolves a rule id that opengrep reported to the rule and the id +// as the rule file states it. Under --experimental, opengrep 1.26.0 reports +// the id as the rule file states it. Without it, opengrep prefixes the id with +// a dotted form of the rule file's directory, so a reported +// "...gitlab-sast-rules.python.eval.python_eval_rule-eval" is the rule whose +// id is "python_eval_rule-eval" in gitlab-sast-rules/python/eval. Ids may +// themselves contain dots ("python_crypto_rule-crypto.hazmat-hash-md5"), so +// the match is the id the report ends with, after a dot, whose rule file's +// directory also appears dotted just before it. More than one such rule is +// refused rather than guessed. +func (p *Pack) RuleByID(reported string) (*Rule, string, bool) { + if r, ok := p.byID[reported]; ok { + return r, reported, true + } + var found *Rule + var foundID string + for id, r := range p.byID { + prefix, ok := strings.CutSuffix(reported, "."+id) + if !ok { + continue + } + dir := strings.ReplaceAll(path.Dir(r.Path), "/", ".") + if !strings.HasSuffix(prefix, dir) { + continue + } + if found != nil && found != r { + return nil, "", false + } + found, foundID = r, id + } + return found, foundID, found != nil +} + +// ToolRuleExcluded reports whether the selection drops a native analyser's +// rule (bandit's B101, for one). +func (p *Pack) ToolRuleExcluded(tool, rule string) bool { return p.excludedTools[tool+"/"+rule] } + +// LoadPack reads and verifies the rule pack rooted at dir. +func LoadPack(dir string) (*Pack, error) { + refuse := func(format string, args ...any) (*Pack, error) { + return nil, fmt.Errorf("%w: %s: %s", ErrRulePack, dir, fmt.Sprintf(format, args...)) + } + selRaw, err := os.ReadFile(filepath.Join(dir, "selection.json")) + if err != nil { + return refuse("%v", err) + } + manRaw, err := os.ReadFile(filepath.Join(dir, "MANIFEST.json")) + if err != nil { + return refuse("%v", err) + } + p := &Pack{Root: dir, SelectionSHA256: sha256Hex(selRaw), byID: map[string]*Rule{}, excludedTools: map[string]bool{}} + if err := strictJSON(selRaw, &p.Selection); err != nil { + return refuse("selection.json: %v", err) + } + if err := strictJSON(manRaw, &p.Manifest); err != nil { + return refuse("MANIFEST.json: %v", err) + } + if p.Selection.Version != 1 || p.Manifest.Version != 1 { + return refuse("selection version %d and manifest version %d; this build reads version 1", p.Selection.Version, p.Manifest.Version) + } + if p.Manifest.SelectionSHA256 != p.SelectionSHA256 { + return refuse("MANIFEST.json was generated from a different selection.json (%s, the file on disk is %s); regenerate it with python -m anvil_eval.vendor_rules", + p.Manifest.SelectionSHA256, p.SelectionSHA256) + } + if err := p.checkCorpora(); err != nil { + return refuse("%v", err) + } + for _, t := range p.Selection.ExcludedToolRules { + p.excludedTools[t.Tool+"/"+t.Rule] = true + } + for _, g := range p.Selection.ExcludedPaths.FileGlobs { + if _, err := path.Match(g, ""); err != nil { + return refuse("excluded file glob %q: %v", g, err) + } + } + + listed := map[string]bool{} + for _, l := range p.Manifest.Licences { + if err := checkFile(dir, l.Path, l.SHA256); err != nil { + return refuse("licence body: %v", err) + } + listed[l.Path] = true + } + for i := range p.Manifest.Rules { + r := &p.Manifest.Rules[i] + if err := p.checkRule(r, listed); err != nil { + return refuse("%s: %v", r.Path, err) + } + if err := checkFile(dir, r.Path, r.SHA256); err != nil { + return refuse("%v", err) + } + for _, id := range r.IDs { + if strings.TrimSpace(id) == "" { + return refuse("%s: a rule has an empty id", r.Path) + } + if prev, dup := p.byID[id]; dup { + return refuse("rule id %q is defined by both %s and %s", id, prev.Path, r.Path) + } + p.byID[id] = r + } + listed[r.Path] = true + } + if len(p.Manifest.Rules) == 0 { + return refuse("the manifest lists no rules") + } + // Nothing unlisted may sit beside the vendored rules. + for _, c := range p.Selection.Corpora { + err := filepath.WalkDir(filepath.Join(dir, c.Name), func(full string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + rel, _ := filepath.Rel(dir, full) + if !listed[filepath.ToSlash(rel)] { + return fmt.Errorf("%s is not in MANIFEST.json", filepath.ToSlash(rel)) + } + return nil + }) + if err != nil { + return refuse("%v", err) + } + } + return p, nil +} + +func (p *Pack) checkCorpora() error { + seen := map[string]bool{} + for _, c := range p.Selection.Corpora { + adm, ok := admittedCorpora[c.Name] + if !ok { + return fmt.Errorf("corpus %q is not an admitted corpus", c.Name) + } + if c.Repository != adm.repository { + return fmt.Errorf("corpus %q names repository %q; it may only come from %q", c.Name, c.Repository, adm.repository) + } + if len(c.Commit) != 40 || strings.Trim(c.Commit, "0123456789abcdef") != "" { + return fmt.Errorf("corpus %q is pinned to %q, which is not a full commit SHA", c.Name, c.Commit) + } + for _, d := range c.Directories { + if !slices.Contains(adm.directories, d) { + return fmt.Errorf("corpus %q directory %q is not on its directory allowlist %v", c.Name, d, adm.directories) + } + } + seen[c.Name] = true + } + if len(seen) != len(p.Selection.Corpora) { + return errors.New("a corpus is listed twice") + } + return nil +} + +func (p *Pack) checkRule(r *Rule, licences map[string]bool) error { + var corpus *Corpus + for i := range p.Selection.Corpora { + if p.Selection.Corpora[i].Name == r.Corpus { + corpus = &p.Selection.Corpora[i] + } + } + switch { + case corpus == nil: + return fmt.Errorf("corpus %q is not in the selection", r.Corpus) + case r.Repository != corpus.Repository || r.Commit != corpus.Commit: + return fmt.Errorf("provenance %s@%s disagrees with the selection's %s@%s", r.Repository, r.Commit, corpus.Repository, corpus.Commit) + case !slices.Contains(AdmittedLicences, r.Licence): + return fmt.Errorf("licence %q is not admitted (%v)", r.Licence, AdmittedLicences) + case strings.TrimSpace(r.LicenceEvidence) == "": + return errors.New("no licence evidence: a licence is read from a body, never assumed") + case !licences[r.LicenceFile]: + return fmt.Errorf("licence body %q is not archived in the manifest", r.LicenceFile) + case len(r.IDs) == 0 || len(r.CWEs) == 0 || len(r.Languages) == 0: + return errors.New("a rule needs an id, a CWE and a language") + case len(r.BlobSHA1) != 40: + return errors.New("no git blob id") + } + inCorpus := strings.TrimPrefix(r.Path, r.Corpus+"/") + if inCorpus == r.Path { + return errors.New("path is outside its corpus directory") + } + ok := false + for _, d := range corpus.Directories { + if strings.HasPrefix(inCorpus, d+"/") { + ok = true + } + } + if !ok { + return fmt.Errorf("path is in none of the corpus's selected directories %v", corpus.Directories) + } + for _, ex := range p.Selection.ExcludedRuleFiles { + if ex.Corpus == r.Corpus && ex.Path == inCorpus { + return errors.New("the selection excludes this rule file") + } + } + for _, ex := range p.Manifest.ExcludedByLicence { + if ex.Path == r.Path { + return fmt.Errorf("the manifest both vendors this rule and excludes it for its %s licence", ex.Licence) + } + } + for _, ex := range p.Selection.ExcludedDirectories { + if ex.Corpus == r.Corpus && strings.HasPrefix(inCorpus, ex.Directory+"/") { + return fmt.Errorf("the selection excludes directory %q", ex.Directory) + } + } + return nil +} + +// checkFile verifies that rel (a slash path under root) exists, is a regular +// file inside root and hashes to want. +func checkFile(root, rel, want string) error { + clean := path.Clean(rel) + if clean != rel || path.IsAbs(clean) || strings.HasPrefix(clean, "../") || clean == ".." { + return fmt.Errorf("%q is not a clean relative path", rel) + } + full := filepath.Join(root, filepath.FromSlash(clean)) + st, err := os.Lstat(full) + if err != nil { + return err + } + if !st.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file", rel) + } + b, err := os.ReadFile(full) + if err != nil { + return err + } + if got := sha256Hex(b); got != want { + return fmt.Errorf("%s hashes to %s, the manifest pins %s", rel, got, want) + } + return nil +} + +func sha256Hex(b []byte) string { + s := sha256.Sum256(b) + return hex.EncodeToString(s[:]) +} + +func strictJSON(b []byte, v any) error { + dec := json.NewDecoder(strings.NewReader(string(b))) + dec.DisallowUnknownFields() + if err := dec.Decode(v); err != nil { + return err + } + if dec.More() { + return errors.New("trailing data") + } + return nil +} + +// Excluded reports whether a repository-relative slash path is outside what +// Lane B reports on: a test, test-data, documentation or example tree, or a +// test file by name (the selection's excluded_paths). +func (p *Pack) Excluded(rel string) bool { + segs := strings.Split(rel, "/") + for _, s := range segs[:len(segs)-1] { + if slices.Contains(p.Selection.ExcludedPaths.Directories, s) { + return true + } + } + base := segs[len(segs)-1] + for _, g := range p.Selection.ExcludedPaths.FileGlobs { + if ok, _ := path.Match(g, base); ok { + return true + } + } + return false +} + +// Languages returns every rule language in the pack, sorted. +func (p *Pack) Languages() []string { + set := map[string]bool{} + for _, r := range p.Manifest.Rules { + for _, l := range r.Languages { + set[l] = true + } + } + out := make([]string, 0, len(set)) + for l := range set { + out = append(out, l) + } + sort.Strings(out) + return out +} + +// Tool is a native analyser or engine the selection pins: its exact version, +// where it comes from, and its licence as read from its body. A tool's own +// rules carry this as their provenance. +type Tool struct { + Name string `json:"name"` + Version string `json:"version"` + Repository string `json:"repository"` + Licence string `json:"licence"` + LicenceEvidence string `json:"licence_evidence"` + Role string `json:"role"` +} + +// Tool returns the pinned tool called name. +func (p *Pack) Tool(name string) (Tool, bool) { + for _, t := range p.Selection.Tools { + if t.Name == name { + return t, true + } + } + return Tool{}, false +} diff --git a/internal/recall/runner.go b/internal/recall/runner.go new file mode 100644 index 0000000..e7bdf00 --- /dev/null +++ b/internal/recall/runner.go @@ -0,0 +1,231 @@ +// This file is the subprocess boundary every recall tool crosses. +// +// SUBPROCESS ONLY. opengrep is an OCaml CLI with no bindings in any language, +// so a process is the only way to run it, and it keeps the LGPL-2.1 engine at +// arm's length: Anvil links none of it. gosec and bandit run the same way, so +// all three share one boundary and one error vocabulary. +// +// A MISSING TOOL IS NEVER A CLEAN SCAN. Every run ends in exactly one of: +// +// the tool is absent *ToolAbsentError (ExitCodeArtefactAbsent) +// the tool ran and failed *ToolFailedError, with its exit code and stderr +// the tool ran and matched nothing a nil error and no findings, plus the +// files it says it scanned +// +// and the last is only ever reached when the tool wrote a report this package +// could parse. Silence is not a report: gosec with -quiet writes nothing at all +// for a package without findings (seen 2026-10-03), so this package never +// passes -quiet to gosec, and an absent or empty report is a failure. +// +// THE SCANNED REPOSITORY DOES NOT GET A VOTE. Each tool can be told by the code +// it scans to look away: `nosemgrep` comments for opengrep, `#nosec` for gosec, +// `# nosec` for bandit, a committed .semgrepignore, a .bandit file in a scanned +// directory, and opengrep's built-in default ignores, which skip tests/ and +// more without saying so. A repository under scan is untrusted, so every one is +// switched off: --disable-nosem, -nosec, --ignore-nosec, and explicit file +// targets (a directory target is what consults ignore files and defaults). +// What Lane B does not report is decided by data/rules/selection.json, in +// review, and nowhere else. + +package recall + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// ExitCodeArtefactAbsent is the exit status a command wrapping this package +// uses when a recall tool is absent: the same reserved value the Trivy +// collector uses, distinct from success and from a failed scan. +const ExitCodeArtefactAbsent = 2 + +// ErrToolAbsent and ErrToolFailed are the two failures every runner reports. +var ( + ErrToolAbsent = errors.New("recall: a recall tool is not installed") + ErrToolFailed = errors.New("recall: a recall tool failed") +) + +// ToolAbsentError names the tool and where it was looked for. +type ToolAbsentError struct { + Tool string + Path string + Err error +} + +func (e *ToolAbsentError) Error() string { + return fmt.Sprintf("recall: %s is not available (looked for %q: %v); install the pinned release named in data/rules/selection.json or set its path under recall: in the operator configuration (exit %d)", + e.Tool, e.Path, e.Err, ExitCodeArtefactAbsent) +} + +// Unwrap lets errors.Is match ErrToolAbsent. +func (e *ToolAbsentError) Unwrap() error { return ErrToolAbsent } + +// ExitCode is the exit status a wrapping command uses. +func (e *ToolAbsentError) ExitCode() int { return ExitCodeArtefactAbsent } + +// ToolFailedError is a tool that ran and did not produce a usable report. +type ToolFailedError struct { + Tool string + ExitCode int + Stderr string + Reason string +} + +func (e *ToolFailedError) Error() string { + msg := fmt.Sprintf("recall: %s failed", e.Tool) + if e.ExitCode != 0 { + msg += fmt.Sprintf(" with exit code %d", e.ExitCode) + } + if e.Reason != "" { + msg += ": " + e.Reason + } + if s := tail(e.Stderr, 600); s != "" { + msg += " (stderr: " + s + ")" + } + return msg +} + +// Unwrap lets errors.Is match ErrToolFailed. +func (e *ToolFailedError) Unwrap() error { return ErrToolFailed } + +func tail(s string, n int) string { + s = strings.Join(strings.Fields(s), " ") + if len(s) > n { + return "…" + s[len(s)-n:] + } + return s +} + +// Exec runs one process. Tests replace it; production uses ProcessExec. +type Exec interface { + // Run executes bin with args in dir. env, when non-nil, replaces the + // environment. It returns the exit code (0 on success) and an error only + // when the process could not be run at all. + Run(ctx context.Context, bin string, args []string, dir string, env []string) (stdout, stderr []byte, exitCode int, err error) +} + +// ProcessExec is the os/exec implementation of Exec. +type ProcessExec struct{} + +// Run implements Exec. +func (ProcessExec) Run(ctx context.Context, bin string, args []string, dir string, env []string) ([]byte, []byte, int, error) { + cmd := exec.CommandContext(ctx, bin, args...) + cmd.Dir = dir + if env != nil { + cmd.Env = env + } + var out, errb bytes.Buffer + cmd.Stdout, cmd.Stderr, cmd.Stdin = &out, &errb, nil + err := cmd.Run() + if ctx.Err() != nil { + return out.Bytes(), errb.Bytes(), -1, ctx.Err() + } + var ee *exec.ExitError + if errors.As(err, &ee) { + return out.Bytes(), errb.Bytes(), ee.ExitCode(), nil + } + return out.Bytes(), errb.Bytes(), 0, err +} + +// Tools names the binaries. An empty field means the tool's own name, looked +// up on PATH; a value with a path separator is used as given. +type Tools struct { + Opengrep string + Gosec string + Bandit string + // GoBin is the directory holding the `go` command gosec loads packages + // with. Empty means the `go` on PATH. + GoBin string +} + +func (t Tools) binary(tool string) string { + switch tool { + case ToolOpengrep: + return t.Opengrep + case ToolGosec: + return t.Gosec + case ToolBandit: + return t.Bandit + } + return "" +} + +// The three tool names, as data/rules/selection.json pins them. +const ( + ToolOpengrep = "opengrep" + ToolGosec = "gosec" + ToolBandit = "bandit" +) + +// resolve finds a tool's executable or returns a *ToolAbsentError. +func resolve(tool, configured string) (string, error) { + name := configured + if name == "" { + name = tool + } + if strings.ContainsRune(name, filepath.Separator) { + st, err := os.Stat(name) + if err != nil { + return "", &ToolAbsentError{Tool: tool, Path: name, Err: err} + } + if st.IsDir() || st.Mode().Perm()&0o111 == 0 { + return "", &ToolAbsentError{Tool: tool, Path: name, Err: errors.New("not an executable file")} + } + return name, nil + } + p, err := exec.LookPath(name) + if err != nil { + return "", &ToolAbsentError{Tool: tool, Path: name, Err: err} + } + return p, nil +} + +// checkVersion runs the tool's version command and refuses a release other +// than the pinned one: provenance names a version, and a candidate attributed +// to a version that did not produce it is a false provenance. +func checkVersion(ctx context.Context, x Exec, tool, bin, want string) error { + var args []string + switch tool { + case ToolOpengrep: + args = []string{"--version"} + case ToolGosec: + args = []string{"-version"} + case ToolBandit: + args = []string{"--version"} + } + out, errb, code, err := x.Run(ctx, bin, args, "", nil) + if err != nil { + return &ToolAbsentError{Tool: tool, Path: bin, Err: err} + } + if code != 0 { + return &ToolFailedError{Tool: tool, ExitCode: code, Stderr: string(errb), Reason: "its version command failed"} + } + got := versionOf(tool, string(out)+string(errb)) + if got != want { + return &ToolFailedError{Tool: tool, Reason: fmt.Sprintf("version %q is not the pinned %q", got, want)} + } + return nil +} + +// versionOf reads the version each tool prints: opengrep "1.26.0", gosec +// "Version: 2.29.0" (among other lines), bandit "bandit 1.9.4" (among others). +func versionOf(tool, out string) string { + for _, line := range strings.Split(out, "\n") { + f := strings.Fields(line) + switch { + case tool == ToolOpengrep && len(f) == 1: + return strings.TrimPrefix(f[0], "v") + case tool == ToolGosec && len(f) == 2 && f[0] == "Version:": + return strings.TrimPrefix(f[1], "v") + case tool == ToolBandit && len(f) >= 2 && f[0] == "bandit": + return f[1] + } + } + return "" +} diff --git a/internal/recall/subprocess_test.go b/internal/recall/subprocess_test.go new file mode 100644 index 0000000..a565c8b --- /dev/null +++ b/internal/recall/subprocess_test.go @@ -0,0 +1,50 @@ +package recall_test + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +// TestTheRecallTierLinksNoTool holds "subprocess only" (plan node opengrep): +// the recall tier and the Lane B pipeline import the standard library and +// Anvil's own packages and nothing else, so no recall tool, binding or rule +// engine can be linked into the anvil binary. Every tool is an exec. +func TestTheRecallTierLinksNoTool(t *testing.T) { + const module = "github.com/Susquehanna-Syntax/Anvil/" + checked := 0 + for _, dir := range []string{".", "../laneb"} { + files, err := filepath.Glob(filepath.Join(dir, "*.go")) + if err != nil { + t.Fatal(err) + } + for _, f := range files { + if strings.HasSuffix(f, "_test.go") { + continue + } + src, err := os.ReadFile(f) + if err != nil { + t.Fatal(err) + } + parsed, err := parser.ParseFile(token.NewFileSet(), f, src, parser.ImportsOnly) + if err != nil { + t.Fatal(err) + } + for _, imp := range parsed.Imports { + p, _ := strconv.Unquote(imp.Path.Value) + first, _, _ := strings.Cut(p, "/") + if strings.Contains(first, ".") && !strings.HasPrefix(p, module) { + t.Errorf("%s imports %q: the recall tier runs its tools as processes and links none", f, p) + } + checked++ + } + } + } + if checked == 0 { + t.Fatal("no imports were checked; the guard read nothing") + } +} diff --git a/internal/recall/symbols.go b/internal/recall/symbols.go new file mode 100644 index 0000000..92d9324 --- /dev/null +++ b/internal/recall/symbols.go @@ -0,0 +1,176 @@ +// This file names the enclosing symbol for the two native analysers, which do +// not report one. opengrep's matches carry the name its own parser found. + +package recall + +import ( + "go/ast" + "go/parser" + "go/token" + "strings" +) + +// goSymbol is the function or method declaration around line, as go/parser +// reads it: "Func", or "Recv.Method" with the receiver's type name. A match +// inside a function literal belongs to the declaration that contains it. A +// file that does not parse, or a line outside every declaration, gives "". +func goSymbol(src []byte, line int) string { + if len(src) == 0 { + return "" + } + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", src, parser.SkipObjectResolution) + if err != nil { + return "" + } + for _, d := range f.Decls { + fd, ok := d.(*ast.FuncDecl) + if !ok { + continue + } + if fset.Position(fd.Pos()).Line > line || fset.Position(fd.End()).Line < line { + continue + } + if fd.Recv == nil || len(fd.Recv.List) == 0 { + return fd.Name.Name + } + return receiverName(fd.Recv.List[0].Type) + "." + fd.Name.Name + } + return "" +} + +func receiverName(e ast.Expr) string { + switch t := e.(type) { + case *ast.StarExpr: + return receiverName(t.X) + case *ast.IndexExpr: + return receiverName(t.X) + case *ast.IndexListExpr: + return receiverName(t.X) + case *ast.Ident: + return t.Name + } + return "" +} + +// pythonSymbol is the chain of class and def blocks around line, read from +// indentation: "Class.method", "function", or "" at module level. A block +// encloses the line only if it starts strictly less indented, and only +// logical-line starts count: a forward pass tracks brackets, strings and +// comments, so the closing line of a def signature split over several lines, +// or a line inside a triple-quoted string, is never read as a block. It is a +// reading of the layout rather than a parse, and it is deterministic, which +// is what identity needs: the same file gives the same answer on every scan. +func pythonSymbol(lines []string, line int) string { + if line < 1 || line > len(lines) { + return "" + } + starts := pythonLogicalStarts(lines) + target := line - 1 + for target > 0 && !starts[target] { + target-- // a match inside a continued line belongs to its logical line + } + limit := indentOf(lines[target]) + if strings.TrimSpace(lines[target]) == "" { + limit = 1 << 30 + } + var chain []string + // A match on a def or class line, signature included, belongs to it. + if name, ok := pythonBlockName(lines[target]); ok { + chain = append(chain, name) + } + for i := target - 1; i >= 0 && limit > 0; i-- { + s := strings.TrimSpace(lines[i]) + if !starts[i] || s == "" || strings.HasPrefix(s, "#") { + continue + } + ind := indentOf(lines[i]) + if ind >= limit { + continue + } + limit = ind + if name, ok := pythonBlockName(lines[i]); ok { + chain = append([]string{name}, chain...) + } + } + return strings.Join(chain, ".") +} + +// pythonBlockName reads the name a def or class line opens. +func pythonBlockName(line string) (string, bool) { + s := strings.TrimSpace(line) + for _, kw := range []string{"def ", "async def ", "class "} { + if rest, ok := strings.CutPrefix(s, kw); ok { + if j := strings.IndexAny(rest, "(:["); j >= 0 { + rest = rest[:j] + } + return strings.TrimSpace(rest), true + } + } + return "", false +} + +// pythonLogicalStarts marks the lines that begin a logical line: not inside +// an open bracket, an open triple-quoted string or a backslash continuation. +func pythonLogicalStarts(lines []string) []bool { + starts := make([]bool, len(lines)) + depth := 0 + quote := "" // the open string's delimiter, one quote character or three + cont := false // the previous line ended in a backslash + for i, l := range lines { + starts[i] = depth == 0 && quote == "" && !cont + cont = false + for j := 0; j < len(l); j++ { + c := l[j] + if quote != "" { + if c == '\\' { + j++ + continue + } + if strings.HasPrefix(l[j:], quote) { + j += len(quote) - 1 + quote = "" + } + continue + } + switch c { + case '#': + j = len(l) + case '\'', '"': + q := string(c) + if strings.HasPrefix(l[j:], q+q+q) { + q += q + q + } + quote = q + j += len(q) - 1 + case '(', '[', '{': + depth++ + case ')', ']', '}': + if depth > 0 { + depth-- + } + case '\\': + cont = j == len(l)-1 + } + } + if len(quote) == 1 { + quote = "" // a one-quote string does not span lines + } + } + return starts +} + +func indentOf(s string) int { + n := 0 + for _, r := range s { + switch r { + case ' ': + n++ + case '\t': + n += 8 - n%8 + default: + return n + } + } + return n +} diff --git a/internal/recall/symbols_test.go b/internal/recall/symbols_test.go new file mode 100644 index 0000000..2cffaab --- /dev/null +++ b/internal/recall/symbols_test.go @@ -0,0 +1,71 @@ +package recall + +import ( + "strings" + "testing" +) + +func TestPythonSymbol(t *testing.T) { + src := strings.Split(`import os + +class Config: + """A docstring +def not_a_def(): + """ + + def from_env( + self, prefix="X", + ) -> bool: + value = os.environ[prefix] + if value: + return eval(value) + + def other(self): + pass + +def top(): + return [ + 1, + ] +x = 1`, "\n") + for line, want := range map[int]string{ + 1: "", // module level + 11: "Config.from_env", // the body under a split signature + 13: "Config.from_env", // nested deeper + 10: "Config.from_env", // the signature's closing line belongs to the def + 16: "Config.other", + 21: "top", // inside a bracket continuation + 22: "", + 5: "Config", // inside a docstring, not a def + } { + if got := pythonSymbol(src, line); got != want { + t.Errorf("line %d (%q): %q, want %q", line, src[line-1], got, want) + } + } +} + +func TestGoSymbol(t *testing.T) { + src := []byte(`package p + +type T[K any] struct{} + +func (t *T[K]) Method() { + f := func() { + _ = 1 + } + f() +} + +func Plain() {} + +var x = 1 +`) + for line, want := range map[int]string{7: "T.Method", 12: "Plain", 14: "", 1: ""} { + if got := goSymbol(src, line); got != want { + t.Errorf("line %d: %q, want %q", line, got, want) + } + } + if got := goSymbol([]byte("not go"), 1); got != "" { + t.Errorf("an unparseable file gave %q", got) + } +} diff --git a/internal/recall/tools.go b/internal/recall/tools.go new file mode 100644 index 0000000..d8518c1 --- /dev/null +++ b/internal/recall/tools.go @@ -0,0 +1,451 @@ +// This file builds each tool's argument vector and parses its JSON report. +// The argument builders are pure functions so a test can assert the safety +// flags without running anything. + +package recall + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" +) + +// rawFinding is one tool-reported match before it becomes a candidate. +type rawFinding struct { + tool string + rule string // the tool's rule id (opengrep's dotted id, gosec's G304, bandit's B602) + absPath string + startLine int + endLine int + cwe string // the tool's own CWE, if it reports one + enclosing []string // opengrep's enclosing context, outermost first (reversed from its report) +} + +// toolRun is one tool's contribution to a scan. +type toolRun struct { + findings []rawFinding + // scanned holds the absolute paths the tool says it read. + scanned map[string]bool + // problems are coverage failures: something was asked of the tool and not + // done. Each one makes the scan incomplete. + problems []string + // partialParses counts files opengrep parsed only in part (C macros, for + // one). Recorded, not treated as incomplete: see Result.PartialParses. + partialParses int + // analysed is how many files gosec says it analysed, per module root. + analysed map[string]int +} + +// maxArgBytes bounds one invocation's argument vector. Linux allows about +// 2 MiB of arguments and environment together; batches stay well under it. +const maxArgBytes = 512 << 10 + +// batches splits targets so no invocation's arguments exceed maxArgBytes. +func batches(fixed int, targets []string) [][]string { + var out [][]string + var cur []string + size := fixed + for _, t := range targets { + if len(cur) > 0 && size+len(t)+1 > maxArgBytes { + out = append(out, cur) + cur, size = nil, fixed + } + cur = append(cur, t) + size += len(t) + 1 + } + if len(cur) > 0 { + out = append(out, cur) + } + return out +} + +// RuleTimeoutSeconds is how long one rule may run on one file: opengrep's own +// default, stated so it cannot drift. On curl on 2026-10-03 a few rules timed +// out on two files at 5 s, and still did at 30 s (different rules each run, +// so the cause is load as much as cost), while 30 s took the scan from 56 s +// to 217 s. A timeout is reported as incomplete coverage either way, so it +// can never mark a finding fixed. +const RuleTimeoutSeconds = 5 + +// OpengrepArgs is opengrep's argument vector for rule files and absolute +// target files. The report is read from stdout: under --experimental, +// opengrep 1.26.0 ignores --output and prints the report (seen 2026-10-03). +// +// - --disable-nosem: a nosemgrep comment in the scanned code is ignored. +// - --disable-version-check: no request to opengrep's servers. +// - --experimental --output-enclosing-context: opengrep's own parser names +// the function and class around each match, which is the candidate's +// function-authoritative identity. +// - --timeout and --timeout-threshold=0: a rule gets RuleTimeoutSeconds on +// a file, and a file is never skipped for its timeouts. A timeout still +// happens, and is reported as incomplete coverage, never silently. +// - targets are files, never a directory: a directory target is what +// consults .semgrepignore and opengrep's default ignore list. +func OpengrepArgs(rules, targets []string) []string { + args := []string{"scan", "--json", "--quiet", "--disable-version-check", + "--disable-nosem", "--experimental", "--output-enclosing-context", "--no-autofix", + "--timeout=" + strconv.Itoa(RuleTimeoutSeconds), "--timeout-threshold=0"} + for _, r := range rules { + args = append(args, "--config="+r) + } + return append(args, targets...) +} + +// GosecArgs is gosec's argument vector over every package under the working +// directory. -nosec ignores #nosec and gosec:disable annotations; -no-fail +// makes the exit code mean "ran" rather than "found"; -quiet is never passed. +func GosecArgs(out string) []string { + return []string{"-fmt=json", "-out=" + out, "-no-fail", "-nosec", "./..."} +} + +// GosecEnv is the environment gosec runs in: the operator's, with the Go +// command pinned to the local toolchain and kept off the network. Package +// loading that would need a download fails instead, and gosec reports it as a +// load error, which Lane B records as incomplete coverage. CGO_ENABLED=0 +// keeps package loading from running the C compiler on the scanned +// repository's cgo code (which can #include any readable host file); the cgo +// files it leaves out show up in the file count as uncovered. +func GosecEnv(goBin string) []string { + env := []string{} + for _, kv := range os.Environ() { + k := kv[:max(strings.IndexByte(kv, '='), 0)] + switch k { + case "GOFLAGS", "GOPROXY", "GOSUMDB", "GOTOOLCHAIN", "GONOSUMDB", "GOPRIVATE", "GONOPROXY", "GOINSECURE", + "CGO_ENABLED", "GOWORK", "GOOS", "GOARCH": + continue + case "PATH": + if goBin != "" { + kv = "PATH=" + goBin + string(os.PathListSeparator) + kv[len("PATH="):] + } + } + env = append(env, kv) + } + return append(env, "GOFLAGS=-mod=readonly", "GOPROXY=off", "GOSUMDB=off", "GOTOOLCHAIN=local", + "CGO_ENABLED=0", "GOWORK=off") +} + +// BanditNoExclude is the -x value Lane B passes to bandit. Without -x, +// bandit's default excludes (.git, CVS, .tox, __pycache__, …) are matched as +// substrings of every path, even of files named explicitly, so +// .github/scripts/rel.py and CVSS-tool/app.py were silently skipped (seen +// 2026-10-03). An empty -x would match every path; this one matches none. +const BanditNoExclude = "/anvil-recall-excludes-nothing" + +// BanditArgs is bandit's argument vector over absolute target files. +// --ignore-nosec ignores `# nosec`; -x replaces bandit's own excludes with +// one that matches nothing; explicit files stop bandit discovering a .bandit +// file in a scanned directory. +func BanditArgs(targets []string, out string) []string { + return append([]string{"-f", "json", "-o", out, "-q", "--ignore-nosec", "-x", BanditNoExclude}, targets...) +} + +func runOpengrep(ctx context.Context, x Exec, bin string, rules, targets []string) (toolRun, error) { + run := toolRun{scanned: map[string]bool{}} + tmp, err := os.MkdirTemp("", "anvil-opengrep-") + if err != nil { + return run, err + } + defer os.RemoveAll(tmp) + fixed := len(strings.Join(OpengrepArgs(rules, nil), " ")) + for _, b := range batches(fixed, targets) { + out, errb, code, err := x.Run(ctx, bin, OpengrepArgs(rules, b), tmp, nil) + if err != nil { + return run, &ToolFailedError{Tool: ToolOpengrep, Reason: err.Error()} + } + if code != 0 && code != 1 { + return run, &ToolFailedError{Tool: ToolOpengrep, ExitCode: code, Stderr: string(errb)} + } + if err := parseOpengrep(out, &run); err != nil { + return run, &ToolFailedError{Tool: ToolOpengrep, ExitCode: code, Stderr: string(errb), Reason: err.Error()} + } + } + return run, nil +} + +type opengrepReport struct { + Results []struct { + CheckID string `json:"check_id"` + Path string `json:"path"` + Start struct { + Line int `json:"line"` + } `json:"start"` + End struct { + Line int `json:"line"` + } `json:"end"` + Extra struct { + EnclosingContext []struct { + Kind string `json:"kind"` + Name string `json:"name"` + } `json:"enclosing_context"` + Metadata struct { + CWE json.RawMessage `json:"cwe"` + } `json:"metadata"` + } `json:"extra"` + } `json:"results"` + Errors []struct { + Level string `json:"level"` + Type json.RawMessage `json:"type"` + Path string `json:"path"` + Message string `json:"message"` + } `json:"errors"` + Paths *struct { + Scanned []string `json:"scanned"` + } `json:"paths"` +} + +func parseOpengrep(raw []byte, run *toolRun) error { + if len(strings.TrimSpace(string(raw))) == 0 { + return fmt.Errorf("the report is empty; an empty report is never read as a clean scan") + } + var rep opengrepReport + if err := json.Unmarshal(raw, &rep); err != nil { + return fmt.Errorf("unparseable report: %w", err) + } + if rep.Paths == nil || rep.Results == nil { + return fmt.Errorf("the report has no results or paths section, so it cannot say what was scanned") + } + for _, p := range rep.Paths.Scanned { + run.scanned[p] = true + } + for _, r := range rep.Results { + // opengrep lists the context innermost first ("from_pyfile", then + // "Config", seen 2026-10-03 on flask); the symbol reads outermost first. + var enc []string + for i := len(r.Extra.EnclosingContext) - 1; i >= 0; i-- { + if n := r.Extra.EnclosingContext[i].Name; n != "" { + enc = append(enc, n) + } + } + run.findings = append(run.findings, rawFinding{ + tool: ToolOpengrep, rule: r.CheckID, absPath: r.Path, + startLine: r.Start.Line, endLine: r.End.Line, enclosing: enc, + }) + } + for _, e := range rep.Errors { + kind := strings.Trim(string(e.Type), `"`) + if strings.HasPrefix(kind, `["PartialParsing"`) || kind == "PartialParsing" { + run.partialParses++ + continue + } + where := e.Path + if where == "" { + where = "the scan" + } + run.problems = append(run.problems, fmt.Sprintf("opengrep %s in %s: %s", e.Level, where, tail(firstLine(e.Message), 200))) + } + return nil +} + +func runGosec(ctx context.Context, x Exec, bin, goBin string, modules []string) (toolRun, error) { + run := toolRun{scanned: map[string]bool{}, analysed: map[string]int{}} + tmp, err := os.MkdirTemp("", "anvil-gosec-") + if err != nil { + return run, err + } + defer os.RemoveAll(tmp) + env := GosecEnv(goBin) + for i, mod := range modules { + out := filepath.Join(tmp, fmt.Sprintf("out-%04d.json", i)) + _, errb, code, err := x.Run(ctx, bin, GosecArgs(out), mod, env) + if err != nil { + return run, &ToolFailedError{Tool: ToolGosec, Reason: err.Error()} + } + if code != 0 { + return run, &ToolFailedError{Tool: ToolGosec, ExitCode: code, Stderr: string(errb)} + } + if err := parseGosec(out, mod, &run); err != nil { + return run, &ToolFailedError{Tool: ToolGosec, Stderr: string(errb), Reason: err.Error()} + } + } + return run, nil +} + +type gosecReport struct { + GolangErrors map[string][]struct { + Line int `json:"line"` + Error string `json:"error"` + } `json:"Golang errors"` + Issues []struct { + RuleID string `json:"rule_id"` + File string `json:"file"` + Line string `json:"line"` + CWE struct { + ID string `json:"id"` + } `json:"cwe"` + } `json:"Issues"` + Stats *struct { + Files int `json:"files"` + } `json:"Stats"` +} + +func parseGosec(file, module string, run *toolRun) error { + raw, err := readReport(file) + if err != nil { + return err + } + var rep gosecReport + if err := json.Unmarshal(raw, &rep); err != nil { + return fmt.Errorf("unparseable report: %w", err) + } + if rep.Stats == nil { + return fmt.Errorf("the report has no Stats section, so it cannot say what was scanned") + } + run.analysed[module] += rep.Stats.Files + files := make([]string, 0, len(rep.GolangErrors)) + for f := range rep.GolangErrors { + files = append(files, f) + } + sort.Strings(files) + for _, f := range files { + errs := rep.GolangErrors[f] + msg := "" + if len(errs) > 0 { + msg = errs[0].Error + } + run.problems = append(run.problems, fmt.Sprintf("gosec could not load %s (%d error(s), first: %s); its packages were analysed without full type information", + relOrSelf(module, f), len(errs), tail(msg, 160))) + } + for _, is := range rep.Issues { + start, end := lineRange(is.Line) + run.findings = append(run.findings, rawFinding{ + tool: ToolGosec, rule: is.RuleID, absPath: is.File, startLine: start, endLine: end, + cwe: cweOf(is.CWE.ID), + }) + } + return nil +} + +func runBandit(ctx context.Context, x Exec, bin string, targets []string) (toolRun, error) { + run := toolRun{scanned: map[string]bool{}} + tmp, err := os.MkdirTemp("", "anvil-bandit-") + if err != nil { + return run, err + } + defer os.RemoveAll(tmp) + fixed := len(strings.Join(BanditArgs(nil, filepath.Join(tmp, "out-0000.json")), " ")) + for i, b := range batches(fixed, targets) { + out := filepath.Join(tmp, fmt.Sprintf("out-%04d.json", i)) + _, errb, code, err := x.Run(ctx, bin, BanditArgs(b, out), tmp, nil) + if err != nil { + return run, &ToolFailedError{Tool: ToolBandit, Reason: err.Error()} + } + if code != 0 && code != 1 { + return run, &ToolFailedError{Tool: ToolBandit, ExitCode: code, Stderr: string(errb)} + } + if err := parseBandit(out, &run); err != nil { + return run, &ToolFailedError{Tool: ToolBandit, ExitCode: code, Stderr: string(errb), Reason: err.Error()} + } + } + return run, nil +} + +type banditReport struct { + Errors []struct { + Filename string `json:"filename"` + Reason string `json:"reason"` + } `json:"errors"` + Results []struct { + TestID string `json:"test_id"` + Filename string `json:"filename"` + LineNumber int `json:"line_number"` + LineRange []int `json:"line_range"` + IssueCWE struct { + ID int `json:"id"` + } `json:"issue_cwe"` + } `json:"results"` + Metrics map[string]json.RawMessage `json:"metrics"` +} + +func parseBandit(file string, run *toolRun) error { + raw, err := readReport(file) + if err != nil { + return err + } + var rep banditReport + if err := json.Unmarshal(raw, &rep); err != nil { + return fmt.Errorf("unparseable report: %w", err) + } + if rep.Results == nil || rep.Metrics == nil { + return fmt.Errorf("the report has no results or metrics section, so it cannot say what was scanned") + } + failed := map[string]bool{} + for _, e := range rep.Errors { + failed[e.Filename] = true + run.problems = append(run.problems, fmt.Sprintf("bandit could not scan %s: %s", e.Filename, tail(e.Reason, 160))) + } + // bandit's metrics hold one entry per file it read, keyed by the path it + // was given, plus "_totals". + for k := range rep.Metrics { + if k != "_totals" && !failed[k] { + run.scanned[k] = true + } + } + for _, r := range rep.Results { + end := r.LineNumber + if n := len(r.LineRange); n > 0 && r.LineRange[n-1] > end { + end = r.LineRange[n-1] + } + cwe := "" + if r.IssueCWE.ID > 0 { + cwe = "CWE-" + strconv.Itoa(r.IssueCWE.ID) + } + run.findings = append(run.findings, rawFinding{ + tool: ToolBandit, rule: r.TestID, absPath: r.Filename, startLine: r.LineNumber, endLine: end, cwe: cwe, + }) + } + return nil +} + +func readReport(file string) ([]byte, error) { + raw, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("no report was written: %w", err) + } + if len(strings.TrimSpace(string(raw))) == 0 { + return nil, fmt.Errorf("the report is empty; an empty report is never read as a clean scan") + } + return raw, nil +} + +var cweNumber = regexp.MustCompile(`(?i)^\s*(?:cwe-?)?0*(\d+)`) + +// cweOf normalises "CWE-078", "cwe-78" and "78" to "CWE-78"; anything else is +// the empty string. +func cweOf(s string) string { + m := cweNumber.FindStringSubmatch(s) + if m == nil || m[1] == "" { + return "" + } + return "CWE-" + m[1] +} + +// lineRange reads gosec's line field, "12" or "12-14". +func lineRange(s string) (int, int) { + a, b, ok := strings.Cut(s, "-") + start, _ := strconv.Atoi(strings.TrimSpace(a)) + end := start + if ok { + if e, err := strconv.Atoi(strings.TrimSpace(b)); err == nil && e >= start { + end = e + } + } + return start, end +} + +func firstLine(s string) string { + s, _, _ = strings.Cut(strings.TrimSpace(s), "\n") + return s +} + +func relOrSelf(base, p string) string { + if r, err := filepath.Rel(base, p); err == nil && !strings.HasPrefix(r, "..") { + return filepath.ToSlash(r) + } + return p +} diff --git a/internal/record/CONTRACT.md b/internal/record/CONTRACT.md index 3ab2bea..a0a01e7 100644 --- a/internal/record/CONTRACT.md +++ b/internal/record/CONTRACT.md @@ -162,10 +162,11 @@ file the scan did not have. Those two demand opposite handling: the first is dro escalated to a human or to the DAST half. A confidence threshold silently discards exactly the second population, and a float cannot express the difference. -Lane B keeps its own in-process `Verdict.Result` (`EXHIBITS|…`), which is a judgment about the **code**; -`anvil/verdict` is a judgment about the **finding**. Collapsing them would lose that distinction, so both -stand and **the Lane B pipeline owns the mapping, including case normalisation**. A mapping with an owner and a test is -not the same thing as two vocabularies drifting. +Lane B was designed to keep its own in-process `Verdict.Result` (`EXHIBITS|…`), a judgment about the **code**, +beside `anvil/verdict`, a judgment about the **finding**, with **the Lane B pipeline owning the mapping**. The +gate deleted the adjudicator that would have produced the first, so as built (Phase 6, 2026-10-03) Lane B has +no in-process verdict and the mapping it owns has one answer: `internal/laneb.VerdictFor` returns +`unconfirmed`, and `TestEveryCandidateReachesTheRecordUnconfirmed` (internal/scan) holds it through the store. --- @@ -735,3 +736,63 @@ this contract: No blocking finding remains open. `anvil/schemaVersion` is `1.0.0` and the wire schema is `schemas/anvil-record-v1.schema.json` (`x-anvil-contractVersion` 1.0.0): record v1 is frozen. + +## Amendment 2026-10-03 — record 1.1.0: rule provenance for Lane B (plan nodes candidatelist and pipeline) + +**Why.** Phase 6's exit gate needs every Lane B finding to carry its rule's provenance (repository, commit or +release, licence) in the record. Frozen v1 had no slot for it: `anvil/detector` names a model, not a rule +corpus, and `anvil/advisory` describes an advisory feed. This is the first change after the freeze, so it is +additive and versioned: `anvil/schemaVersion` and the wire schema's `x-anvil-contractVersion` are now `1.1.0` +(`TestTheSchemaNamesTheContractVersion` holds them together), and a 1.0.0 record is a valid 1.1.0 record. + +**The slots, all SARIF-native or on a SARIF property bag.** + +| Field | Native / ext | Required? | Producer | Consumer | +|---|---|---|---|---| +| `run.tool.extensions[]` | SARIF-native §3.18.3 | optional | the Lane B pipeline: one per rule corpus or native analyser it ran (`gitlab-sast-rules`, `0xdea-semgrep-rules`, `gosec`, `bandit`), each holding the rules its results cite | report, triage gate, audit | +| `toolComponent.rules[].properties["anvil/ruleProvenance"]` | ext | optional; when present every field is non-blank: `source`, `repository`, `version`, `rulePath`, `licenseSpdx`, `licenseEvidence` | the Lane B pipeline, from `data/rules/MANIFEST.json` or the tool's pin in `data/rules/selection.json` | report, licence review | +| `result.rule` | SARIF-native §3.27.7 | optional; when present it names a tool extension by index, agrees with `ruleId`, and resolves to a rule with complete `anvil/ruleProvenance` | the Lane B pipeline, on every result it emits | report, triage gate | + +`(*Run).validate` checks the resolution, which a JSON Schema cannot (`TestARuleReferenceMustResolveToCompleteProvenance`); +the wire schema checks the shapes and the non-blank fields (`ruleProvenance`, `toolExtension`, `result.rule`), +and refused both mutations tried on 2026-10-03 (a blank `licenseSpdx`, a `result.rule` with no tool component). +The rule is "if present, complete" rather than "required on every SAST result", because SAST results that +predate Lane B (test fixtures, the read path's) carry no rule; the Lane B pipeline guarantees presence on its +own results, and `TestEveryCandidateReachesTheRecordUnconfirmed` (internal/scan) holds that through the store. + +**How Lane B fills the rest of a result, with no schema change.** + +* `anvil/verdict` `unconfirmed`, `anvil/confidence` 1, `anvil/detector` kind `sast` with an empty model and + revision, no `result.rank` (no ranker ships in v1), `anvil/evidenceClass` `sast_static_only`, and + `result.kind` `open` (SARIF's "could not determine whether this is a problem"). +* `anvil/reasoning` is composed from a closed vocabulary and integers (`internal/laneb`, `composeReasoning`), so + it alone is `anvil_generated`; the message, snippet, context, path and symbol are under the `untrusted` + default. +* `partialFingerprints["anvilFindingId/v1"]` is the SAST tier of anvil-fp/v1, with `rule_id_versioned` + `/@` (or `/@`), so a rule + file that changes re-mints only its own findings, and `enclosing_symbol_path` `path::Symbol` from a parser. +* `partialFingerprints["primaryLocationLineHash"]` is required once a region has a start line, and nothing + produced it before. Lane B writes the first 16 hex characters of the SHA-256 of the match's first line with + whitespace collapsed, then `:` and its occurrence in the file. It is Anvil's own construction, not CodeQL's; + who owns this key is still the open question §6 and the GitHub projection record. +* `result.taxa` cites CWE 4.20 in `run.taxonomies`. +* `anvil/advisorySnapshot` is required on the SAST run. When repository SCA ran, it is Trivy's as before; when + only Lane B ran, it names the rule pack: feed id `laneb-rule-pack`, digest `laneb/selection@<12 hex of + selection.json's SHA-256>`, dated by the owner's selection decision. +* `anvil/specHarvest` on the SAST run is now produced (`internal/laneb`, `Harvest`), with `declaredFormat` + left empty so the dynamic tier classifies from the bytes. + +**Trust of the run-level strings.** `tool.extensions` names, rule ids and `anvil/ruleProvenance` fields come +from the rule corpora and the tools' pins: third-party text, outside any result's `anvil/trust` assertion. They +are never `anvil_generated`, and a consumer (the triage prompt above all) treats them as untrusted data. + +**A limit of the exclusions.** Lane B does not report on test, test-data, documentation and example trees +(`data/rules/selection.json`, `excluded_paths`), and those are matched by name, so a repository can put +first-party code under `spec/` or `docs/` and Lane B will not see it. Moving code there makes its findings +absent, and the store marks them fixed. The owner kept the exclusions on 2026-10-03 (without them curl's full +scan measured 525, over budget); a consumer treats a Lane B finding that became fixed with no change to its +code as suspect. + +**The store.** Migration `0002_recall_candidates` adds `scan_run.recall_candidates`, the candidates-per-scan +count (NULL when Lane B did not run, 0 when it ran and matched nothing, negative refused). `schema.sql` is +unchanged. diff --git a/internal/record/assemble.go b/internal/record/assemble.go index 26ccdc9..33bf4c9 100644 --- a/internal/record/assemble.go +++ b/internal/record/assemble.go @@ -32,11 +32,23 @@ type Assembly struct { // SastTool is the SAST half's tool.driver. SastTool ToolComponent + // SastExtensions are the SAST half's tool.extensions: Lane B's rule + // corpora and native analysers, with the rules its results cite. + SastExtensions []ToolComponent + + // SastTaxonomies are the taxonomies the SAST half's results cite in + // result.taxa: CWE, for Lane B. + SastTaxonomies []ToolComponent + // SastResults are the SAST half's results, as emitted. SastResults []Result // AdvisorySnapshot identifies the advisory corpus the SAST half read. AdvisorySnapshot *AdvisorySnapshot + + // SpecHarvest is the SAST half's anvil/specHarvest (Lane B's spec + // harvest); nil when no harvest ran in this scan. + SpecHarvest *SpecHarvest } // ErrAssembleDastHalf means the audit's DAST half ran. Assembling a DAST run @@ -62,7 +74,8 @@ func Assemble(a Assembly) (*SARIFLog, error) { results = []Result{} } sast := Run{ - Tool: Tool{Driver: a.SastTool}, + Tool: Tool{Driver: a.SastTool, Extensions: a.SastExtensions}, + Taxonomies: a.SastTaxonomies, AutomationDetails: RunAutomationDetails{ ID: "anvil/" + string(HalfSast) + "/" + s.AuditID, CorrelationGUID: s.AuditID, @@ -73,6 +86,7 @@ func Assemble(a Assembly) (*SARIFLog, error) { Status: s.Sast.Status, SealedAt: s.Sast.SealedAt, AdvisorySnapshot: a.AdvisorySnapshot, + SpecHarvest: a.SpecHarvest, }, } diff --git a/internal/record/contract.go b/internal/record/contract.go index 5e70729..a096dbf 100644 --- a/internal/record/contract.go +++ b/internal/record/contract.go @@ -78,7 +78,12 @@ const ( // SchemaVersion is the value of `anvil/schemaVersion`. Bump the minor // component for an additive change, the major for a breaking one; the // store's migration ledger keys off it. - SchemaVersion = "1.0.0" + // + // 1.1.0 (2026-10-03, Phase 6): rule provenance for Lane B. tool.extensions, + // result.rule and a rule descriptor's anvil/ruleProvenance; see + // CONTRACT.md, the amendment of that date. A 1.0.0 record is a valid + // 1.1.0 record. + SchemaVersion = "1.1.0" ) // Fingerprint identifiers. The fingerprint owns the algorithm; this file owns the key @@ -1700,6 +1705,11 @@ type Result struct { Taxa []ReportingDescriptorReference `json:"taxa,omitempty"` + // Rule is SARIF §3.27.7: the rule this result cites, by id and by the + // index of the tool extension that holds it (record 1.1.0). A result that + // carries it must resolve to a rule with complete anvil/ruleProvenance. + Rule *ReportingDescriptorReference `json:"rule,omitempty"` + // WebRequest and WebResponse are the SARIF-native DAST evidence slots // (§3.27.14/15). MASKED BY secrets masking BEFORE STORAGE — research/18 Risk #10: // "an 8-hour TTL is not a security control for a token that is still @@ -2122,6 +2132,10 @@ type Message struct { // Tool is SARIF §3.18. type Tool struct { Driver ToolComponent `json:"driver"` + // Extensions are SARIF §3.18.3 tool extensions: for the SAST half, the + // rule corpora and native analysers Lane B ran, each carrying the rules + // its results cite (record 1.1.0). + Extensions []ToolComponent `json:"extensions,omitempty"` } // ToolComponent is SARIF §3.19. Also used for taxonomies (e.g. CWE). @@ -2147,6 +2161,49 @@ type ReportingDescriptor struct { HelpURI string `json:"helpUri,omitempty"` DefaultConfiguration *ReportingConfiguration `json:"defaultConfiguration,omitempty"` Relationships []ReportingDescriptorRelationship `json:"relationships,omitempty"` + // Properties carries anvil/ruleProvenance on a rule a Lane B result + // cites (record 1.1.0). + Properties *RuleProperties `json:"properties,omitempty"` +} + +// RuleProperties is the typed `anvil/*` bag on a rule descriptor. +type RuleProperties struct { + RuleProvenance *RuleProvenance `json:"anvil/ruleProvenance,omitempty"` +} + +// PropRuleProvenance is the rule descriptor key for RuleProvenance. +const PropRuleProvenance = "anvil/ruleProvenance" + +// RuleProvenance is where a rule came from: the corpus or native analyser, +// its repository, the commit or release, the rule file or the tool's rule id, +// and its licence as read from a body. `anvil/ruleProvenance`, record 1.1.0. +// +// Every field is required and non-blank. A rule whose provenance cannot be +// stated in full is one Lane B may not run (plan node candidatelist: 100% +// non-empty provenance), so a partial one is refused rather than recorded. +type RuleProvenance struct { + Source string `json:"source"` + Repository string `json:"repository"` + Version string `json:"version"` + RulePath string `json:"rulePath"` + LicenseSpdx string `json:"licenseSpdx"` + LicenseEvidence string `json:"licenseEvidence"` +} + +// Validate refuses a provenance with any blank field. +func (p *RuleProvenance) Validate() error { + if p == nil { + return fmt.Errorf("%s is required", PropRuleProvenance) + } + for _, f := range []struct{ name, v string }{ + {"source", p.Source}, {"repository", p.Repository}, {"version", p.Version}, + {"rulePath", p.RulePath}, {"licenseSpdx", p.LicenseSpdx}, {"licenseEvidence", p.LicenseEvidence}, + } { + if strings.TrimSpace(f.v) == "" { + return fmt.Errorf("%s.%s is blank; a rule's provenance is stated in full or not at all", PropRuleProvenance, f.name) + } + } + return nil } // ReportingConfiguration is SARIF §3.50. @@ -2610,14 +2667,58 @@ func (r *Run) validate(auditID string) error { if err := ValidateSpecHarvest(r.Properties.SpecHarvest); err != nil { return err } + for i, ext := range r.Tool.Extensions { + if strings.TrimSpace(ext.Name) == "" { + return fmt.Errorf("tool.extensions[%d] has no name", i) + } + for j := range ext.Rules { + if p := ext.Rules[j].Properties; p != nil && p.RuleProvenance != nil { + if err := p.RuleProvenance.Validate(); err != nil { + return fmt.Errorf("tool.extensions[%d].rules[%d]: %w", i, j, err) + } + } + } + } for i := range r.Results { if err := r.Results[i].validate(r.Properties.Half); err != nil { return fmt.Errorf("results[%d]: %w", i, err) } + if err := r.resolveRule(&r.Results[i]); err != nil { + return fmt.Errorf("results[%d]: %w", i, err) + } } return nil } +// resolveRule checks a result's SARIF result.rule: when present, it names a +// tool extension by index and a rule in it by id, the id agrees with +// result.ruleId, and that rule states its provenance in full. +func (r *Run) resolveRule(res *Result) error { + ref := res.Rule + if ref == nil { + return nil + } + if ref.ToolComponent == nil || ref.ToolComponent.Index == nil { + return fmt.Errorf("result.rule must name its tool extension by index") + } + i := *ref.ToolComponent.Index + if i < 0 || i >= len(r.Tool.Extensions) { + return fmt.Errorf("result.rule names tool.extensions[%d], and the run has %d", i, len(r.Tool.Extensions)) + } + if ref.ID != res.RuleID { + return fmt.Errorf("result.rule.id %q disagrees with result.ruleId %q", ref.ID, res.RuleID) + } + for _, d := range r.Tool.Extensions[i].Rules { + if d.ID == ref.ID { + if d.Properties == nil { + return fmt.Errorf("rule %q in tool.extensions[%d] carries no %s", ref.ID, i, PropRuleProvenance) + } + return d.Properties.RuleProvenance.Validate() + } + } + return fmt.Errorf("result.rule %q is not among tool.extensions[%d]'s rules", ref.ID, i) +} + // ValidateDastCoverage checks that coverage is reported as a numerator and a // denominator whose ratio matches the stated EndpointCoverage — never as a // bare, unfalsifiable percentage — and that ServerLineCoverage is null rather diff --git a/internal/record/contract_test.go b/internal/record/contract_test.go index cdddc4a..db007e6 100644 --- a/internal/record/contract_test.go +++ b/internal/record/contract_test.go @@ -943,3 +943,63 @@ func TestAnUnconfirmedFindingWaitsForTheTriageGate(t *testing.T) { t.Errorf("blockers %q must name the triage gate and must not demote to report-only", blockers) } } + +// TestARuleReferenceMustResolveToCompleteProvenance holds record 1.1.0's one +// rule: a result that cites a rule through result.rule names a tool extension +// that holds it, agrees with ruleId, and the rule states its provenance in +// full. A result with no result.rule is a 1.0.0 shape and stays valid. +func TestARuleReferenceMustResolveToCompleteProvenance(t *testing.T) { + zero, one := 0, 1 + prov := func() *RuleProvenance { + return &RuleProvenance{Source: "gosec", Repository: "https://github.com/securego/gosec", Version: "2.29.0", + RulePath: "gosec:G204", LicenseSpdx: "Apache-2.0", LicenseEvidence: "LICENSE.txt"} + } + build := func(edit func(*Run, *Result)) error { + run := Run{Tool: Tool{Extensions: []ToolComponent{{Name: "gosec", Rules: []ReportingDescriptor{ + {ID: "gosec/G204@2.29.0", Properties: &RuleProperties{RuleProvenance: prov()}}, + }}}}} + res := Result{RuleID: "gosec/G204@2.29.0", Rule: &ReportingDescriptorReference{ + ID: "gosec/G204@2.29.0", ToolComponent: &ToolComponentRef{Index: &zero}}} + edit(&run, &res) + return run.resolveRule(&res) + } + if err := build(func(*Run, *Result) {}); err != nil { + t.Fatalf("a resolvable rule was refused: %v", err) + } + if err := build(func(_ *Run, r *Result) { r.Rule = nil }); err != nil { + t.Fatalf("a 1.0.0 result with no result.rule was refused: %v", err) + } + for name, edit := range map[string]func(*Run, *Result){ + "no extension index": func(_ *Run, r *Result) { r.Rule.ToolComponent = nil }, + "an index past the end": func(_ *Run, r *Result) { r.Rule.ToolComponent.Index = &one }, + "ruleId disagrees": func(_ *Run, r *Result) { r.RuleID = "gosec/G101@2.29.0" }, + "no such rule": func(run *Run, _ *Result) { run.Tool.Extensions[0].Rules[0].ID = "other" }, + "no provenance": func(run *Run, _ *Result) { run.Tool.Extensions[0].Rules[0].Properties = nil }, + "a blank licence evidence": func(run *Run, _ *Result) { + run.Tool.Extensions[0].Rules[0].Properties.RuleProvenance.LicenseEvidence = " " + }, + "a blank repository": func(run *Run, _ *Result) { run.Tool.Extensions[0].Rules[0].Properties.RuleProvenance.Repository = "" }, + } { + if err := build(edit); err == nil { + t.Errorf("%s: accepted", name) + } + } +} + +// TestTheSchemaNamesTheContractVersion: the wire schema's contract version is +// the record's anvil/schemaVersion, so a change to one is a change to both. +func TestTheSchemaNamesTheContractVersion(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("..", "..", "schemas", "anvil-record-v1.schema.json")) + if err != nil { + t.Fatal(err) + } + var doc struct { + Version string `json:"x-anvil-contractVersion"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + t.Fatal(err) + } + if doc.Version != SchemaVersion { + t.Fatalf("the wire schema is contract version %q, the record writes %q", doc.Version, SchemaVersion) + } +} diff --git a/internal/record/readpath_test.go b/internal/record/readpath_test.go index a364fe3..2af265a 100644 --- a/internal/record/readpath_test.go +++ b/internal/record/readpath_test.go @@ -2920,9 +2920,11 @@ func gateUngatedAllowlist() map[string]gateExemption { "Result.ExternalStringPointers": {body: "b598635db55a205e", reason: "a pure accessor on a Result the caller already holds. " + "It cannot obtain one: whoever calls it got the Result from somewhere, and that " + "somewhere is what the gate covers."}, - "Assemble": {body: "adedd1ebaf48cab2", reason: "the record assembler, producer side. It builds a record from " + + "Assemble": {body: "7e5bdb35b8560cb5", reason: "the record assembler, producer side. It builds a record from " + "results its caller already holds, before any half is consumable, and hands the same results " + - "back inside the record; it reads nothing from a sealed half, a store or a packet."}, + "back inside the record; it reads nothing from a sealed half, a store or a packet. Re-read " + + "2026-10-03 for record 1.1.0 (Phase 6): the change copies the caller's own tool extensions, " + + "taxonomies and spec harvest into the run it builds, and reads nothing else."}, "ValidateResultTrust": {body: "bbff534e61dffe8b", reason: "a validator over one caller-held Result, returning only an error. " + "Re-read 2026-10-03 after plan node contractgaps added the validation-step check: the new " + "branch reads the caller's own Result through verifiedLabels and puts JSON Pointers and the " + diff --git a/internal/scan/laneb_test.go b/internal/scan/laneb_test.go new file mode 100644 index 0000000..091d4db --- /dev/null +++ b/internal/scan/laneb_test.go @@ -0,0 +1,146 @@ +package scan + +import ( + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Susquehanna-Syntax/Anvil/internal/laneb" + "github.com/Susquehanna-Syntax/Anvil/internal/recall/recalltest" + "github.com/Susquehanna-Syntax/Anvil/internal/record" + "github.com/Susquehanna-Syntax/Anvil/internal/store" +) + +func laneBRequest(t *testing.T, e env, root string) Request { + t.Helper() + req := e.req + req.Kind, req.RepoPath, req.TargetName = KindRepo, root, "laneb-fixture" + req.LaneB = &laneb.Config{Rules: recalltest.RulePack(), Tools: recalltest.Tools(), Exec: &recalltest.Replay{Root: root}} + return req +} + +// TestEveryCandidateReachesTheRecordUnconfirmed is the Lane B pipeline's +// integration test of its verdict mapping: every recall candidate, through the +// scan controller, the assembler, the record's own validation and the store, +// arrives as anvil/verdict unconfirmed with confidence 1, no model, no rank, +// and a rule whose provenance resolves in full. +func TestEveryCandidateReachesTheRecordUnconfirmed(t *testing.T) { + e := newEnv(t) + root, err := filepath.Abs(recalltest.FixtureRoot()) + if err != nil { + t.Fatal(err) + } + res, err := Run(context.Background(), laneBRequest(t, e, root)) + if err != nil { + t.Fatal(err) + } + if res.Outcome != OutcomeFindings || res.Emitted != 12 || res.RecallCandidates == nil || *res.RecallCandidates != 12 { + t.Fatalf("outcome %q emitted %d candidates %v problems %v", res.Outcome, res.Emitted, res.RecallCandidates, res.Problems) + } + if len(res.NotRun) != 1 || !strings.Contains(res.NotRun[0], "repository SCA did not run") { + t.Errorf("not run: %v", res.NotRun) + } + run := res.Log.Runs[0] + if len(run.Results) != 12 || run.Properties.SpecHarvest == nil || len(run.Properties.SpecHarvest.Files) != 1 { + t.Fatalf("results %d spec harvest %+v", len(run.Results), run.Properties.SpecHarvest) + } + for _, r := range run.Results { + p := r.Properties + if p.Verdict != record.VerdictUnconfirmed || p.Confidence != 1 || p.Detector.Kind != record.DetectorKindSast || + p.Detector.Model != "" || p.Detector.Revision != "" || r.Rank != nil || p.Half != record.HalfSast { + t.Errorf("%s: %+v rank %v", r.RuleID, p, r.Rank) + } + ext := run.Tool.Extensions[*r.Rule.ToolComponent.Index] + var prov *record.RuleProvenance + for _, d := range ext.Rules { + if d.ID == r.RuleID { + prov = d.Properties.RuleProvenance + } + } + if err := prov.Validate(); err != nil { + t.Errorf("%s: %v", r.RuleID, err) + } + } + if snap := run.Properties.AdvisorySnapshot; snap == nil || snap.FeedIDs[0] != "laneb-rule-pack" { + t.Errorf("advisory snapshot %+v", snap) + } + + var verdicts, unconfirmed, count int + if err := e.store.QueryRow(`SELECT count(*), sum(verdict = 'unconfirmed') FROM finding`).Scan(&verdicts, &unconfirmed); err != nil { + t.Fatal(err) + } + if verdicts != 12 || unconfirmed != 12 { + t.Fatalf("the store holds %d findings, %d unconfirmed", verdicts, unconfirmed) + } + if err := e.store.QueryRow(`SELECT recall_candidates FROM scan_run`).Scan(&count); err != nil || count != 12 { + t.Fatalf("scan_run.recall_candidates = %d (%v)", count, err) + } + + // The same tree again: every finding persists, none is new or fixed. + again, err := Run(context.Background(), laneBRequest(t, e, root)) + if err != nil { + t.Fatal(err) + } + marks := map[store.MarkKind]int{} + for _, m := range again.Write.Marks { + marks[m.Kind]++ + } + if marks[store.MarkPersisting] != 12 || len(marks) != 1 { + t.Fatalf("second scan marks %v", marks) + } +} + +// TestALaneBScanOfNothingIsNotClean: a tree with no source in a covered +// language is refused, never reported clean. +func TestALaneBScanOfNothingIsNotClean(t *testing.T) { + e := newEnv(t) + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("docs only\n"), 0o644); err != nil { + t.Fatal(err) + } + res, err := Run(context.Background(), laneBRequest(t, e, root)) + if err != nil { + t.Fatal(err) + } + if res.Outcome != OutcomeRefused || res.Complete { + t.Fatalf("outcome %q complete %v problems %v", res.Outcome, res.Complete, res.Problems) + } + var count int + if err := e.store.QueryRow(`SELECT recall_candidates FROM scan_run`).Scan(&count); err != nil || count != 0 { + t.Fatalf("recall_candidates = %d (%v); a scan that ran and found nothing records 0, not NULL", count, err) + } +} + +// TestAMissingRulePackIsAMissingTool: a refused rule pack stops the scan +// before any audit exists. +func TestAMissingRulePackIsAMissingTool(t *testing.T) { + e := newEnv(t) + root, _ := filepath.Abs(recalltest.FixtureRoot()) + req := laneBRequest(t, e, root) + req.LaneB.Rules = filepath.Join(t.TempDir(), "no-rules") + if _, err := Run(context.Background(), req); !errors.Is(err, ErrMissingTool) { + t.Fatalf("err = %v, want ErrMissingTool", err) + } + var n int + if err := e.store.QueryRow(`SELECT count(*) FROM scan_run`).Scan(&n); err != nil || n != 0 { + t.Fatalf("a refused scan left %d scan runs (%v)", n, err) + } +} + +// TestAPolicyThatExcludesEveryEnabledLaneRefuses: Lane B on, SCA off, and a +// policy whose detectors are sca only, leaves nothing to run. +func TestAPolicyThatExcludesEveryEnabledLaneRefuses(t *testing.T) { + e := newEnv(t) + root, _ := filepath.Abs(recalltest.FixtureRoot()) + req := laneBRequest(t, e, root) + req.PolicyPath = filepath.Join(t.TempDir(), "policy.yml") + if err := os.WriteFile(req.PolicyPath, []byte("version: 1\ndefaults:\n detectors: [sca]\n"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := Run(context.Background(), req); !errors.Is(err, ErrPolicyRefused) { + t.Fatalf("err = %v, want ErrPolicyRefused", err) + } +} diff --git a/internal/scan/run.go b/internal/scan/run.go index 610b28e..935dfdc 100644 --- a/internal/scan/run.go +++ b/internal/scan/run.go @@ -1,6 +1,15 @@ // This file is the scan itself: trigger policy, controller, collection, // comparison, emission, seal, assembly, masking, the store and the SARIF // output, in that order (plan node cli). +// +// A repository scan runs two lanes into the one SAST half: Lane A's +// repository SCA (Trivy, when the operator enables its database) and Lane B's +// recall tier (when the operator configures its rule pack and tools). Each +// lane runs only if the configuration enables it and the trigger policy's +// detectors allow it. A lane the configuration leaves off is stated in +// Result.NotRun, and a scan with a lane not run and nothing found exits +// refused, never clean: no scan reports a repository clean on the strength of +// half its lanes. package scan @@ -20,6 +29,7 @@ import ( "github.com/Susquehanna-Syntax/Anvil/internal/collector/repo" "github.com/Susquehanna-Syntax/Anvil/internal/ingest/cache" "github.com/Susquehanna-Syntax/Anvil/internal/ingest/config" + "github.com/Susquehanna-Syntax/Anvil/internal/laneb" "github.com/Susquehanna-Syntax/Anvil/internal/match" "github.com/Susquehanna-Syntax/Anvil/internal/policy" "github.com/Susquehanna-Syntax/Anvil/internal/record" @@ -72,6 +82,10 @@ type Request struct { TrivyDB TrivyDB Trivy repo.Config + // LaneB configures Lane B's recall tier for repository scans. Nil means + // the operator's configuration leaves Lane B off. + LaneB *laneb.Config + AnvilVersion string Now func() time.Time } @@ -97,6 +111,15 @@ type Result struct { Complete bool Problems []string PolicyHit []string + + // NotRun names each lane the operator's configuration left off. It keeps + // a scan with nothing found from reading clean. + NotRun []string + // Notes are informational: Lane B's candidate count and its budget flag. + Notes []string + // RecallCandidates is Lane B's candidates-per-scan count; nil when Lane B + // did not run. + RecallCandidates *int } // ErrPolicyRefused means the trigger policy resolved to a rule whose @@ -107,6 +130,12 @@ var ErrPolicyRefused = errors.New("scan: the trigger policy does not run this sc // ErrMissingTool wraps a missing scanner or its data. var ErrMissingTool = errors.New("scan: a required tool or its data is missing") +// The two NotRun statements. +const ( + notRunSCA = "repository SCA did not run: the operator's configuration does not enable the Trivy database (trivyDB: {enabled: true})" + notRunLaneB = "Lane B did not run: the operator's configuration does not enable it (recall: {enabled: true})" +) + // Run performs one scan. A returned error with ErrMissingTool, ErrPolicyRefused // or ErrRepoSCANotEnabled happened BEFORE anything was recorded; any other // failure after the audit began is recorded as a failed half and returned in @@ -120,11 +149,10 @@ func Run(ctx context.Context, req Request) (Result, error) { return Result{}, errors.New("scan: a scan needs the store and the advisory cache") } - detector, err := laneDetector(req.Kind) - if err != nil { - return Result{}, err + if req.Kind != KindRepo && req.Kind != KindHost { + return Result{}, fmt.Errorf("scan: unknown scan kind %q", req.Kind) } - rule, polRef, err := resolvePolicy(req, detector) + rule, polRef, err := resolvePolicy(req) if err != nil { return Result{}, err } @@ -132,13 +160,29 @@ func Run(ctx context.Context, req Request) (Result, error) { // Everything that can stop a scan before it starts is checked first, so // a refusal leaves no audit behind. var ( - locator string - trivyDB repo.DatabaseInfo + locator string + trivyDB repo.DatabaseInfo + lane *laneb.Lane + runSCA bool + detected []record.DetectorKind + notRun []string ) switch req.Kind { case KindRepo: + if !req.TrivyDB.Enabled && req.LaneB == nil { + return Result{}, fmt.Errorf("%w, and Lane B is not enabled either, so a repository scan has no lane to run", ErrRepoSCANotEnabled) + } + runSCA = req.TrivyDB.Enabled && policyAllows(rule, record.DetectorKindSCA) + runLaneB := req.LaneB != nil && policyAllows(rule, record.DetectorKindSast) + if !runSCA && !runLaneB { + return Result{}, fmt.Errorf("%w: the resolved rule's detectors are %v, which exclude every enabled repository lane", + ErrPolicyRefused, rule.Detectors) + } if !req.TrivyDB.Enabled { - return Result{}, ErrRepoSCANotEnabled + notRun = append(notRun, notRunSCA) + } + if req.LaneB == nil { + notRun = append(notRun, notRunLaneB) } abs, err := filepath.Abs(req.RepoPath) if err != nil { @@ -152,14 +196,30 @@ func Run(ctx context.Context, req Request) (Result, error) { if req.TargetName != "" { locator = "repo:" + req.TargetName } - if _, err := repo.ResolveBinary(binaryOf(req.Trivy)); err != nil { - return Result{}, fmt.Errorf("%w: %v", ErrMissingTool, err) + if runSCA { + if _, err := repo.ResolveBinary(binaryOf(req.Trivy)); err != nil { + return Result{}, fmt.Errorf("%w: %v", ErrMissingTool, err) + } + trivyDB, err = repo.Database(ctx, req.Trivy.Runner()) + if err != nil { + return Result{}, fmt.Errorf("%w: %v", ErrMissingTool, err) + } + detected = append(detected, record.DetectorKindSCA) } - trivyDB, err = repo.Database(ctx, req.Trivy.Runner()) - if err != nil { - return Result{}, fmt.Errorf("%w: %v", ErrMissingTool, err) + if runLaneB { + // A refused rule pack, an absent tool and a tool at the wrong + // version all stop the scan here, before any audit exists. + if lane, err = laneb.Prepare(ctx, *req.LaneB, abs); err != nil { + return Result{}, fmt.Errorf("%w: %v", ErrMissingTool, err) + } + detected = append(detected, record.DetectorKindSast) } case KindHost: + if !policyAllows(rule, record.DetectorKindHost) { + return Result{}, fmt.Errorf("%w: the resolved rule's detectors are %v, which exclude %q", + ErrPolicyRefused, rule.Detectors, record.DetectorKindHost) + } + detected = []record.DetectorKind{record.DetectorKindHost} if req.Inventory == nil { return Result{}, errors.New("scan: a host scan needs the collector's inventory") } @@ -185,13 +245,15 @@ func Run(ctx context.Context, req Request) (Result, error) { return Result{}, err } - res := Result{AuditID: auditID, Complete: true, PolicyHit: rule.MatchedNames()} + res := Result{AuditID: auditID, Complete: true, PolicyHit: rule.MatchedNames(), NotRun: notRun} emitter := lanea.Emitter{TargetID: locator, AssembledAt: started} var ( emissions []lanea.Emission snapshot *record.AdvisorySnapshot - engineVer string + engineVer []string + ruleset = []string{RulesetVersion} half = record.HalfStatusSealed + lbOut laneb.Output ) incomplete := func(format string, args ...any) { res.Complete = false @@ -206,22 +268,47 @@ func Run(ctx context.Context, req Request) (Result, error) { incomplete("%v", err) } case KindRepo: - engineVer = "trivy " + trivyDB.TrivyVersion - emissions, err = repoLane(ctx, req, trivyDB, emitter, incomplete) - if err != nil { - half = record.HalfStatusFailed - incomplete("%v", err) + if runSCA { + engineVer = append(engineVer, "trivy "+trivyDB.TrivyVersion) + emissions, err = repoLane(ctx, req, trivyDB, emitter, incomplete) + if err != nil { + half = record.HalfStatusFailed + incomplete("%v", err) + } + snapshot = &record.AdvisorySnapshot{ + FeedIDs: []string{TrivyDBSource}, + SnapshotDigest: TrivyDBSource + "@" + trivyDB.UpdatedAt.UTC().Format(time.RFC3339), + ScrapedAt: trivyDB.UpdatedAt.UTC(), + } } - snapshot = &record.AdvisorySnapshot{ - FeedIDs: []string{TrivyDBSource}, - SnapshotDigest: TrivyDBSource + "@" + trivyDB.UpdatedAt.UTC().Format(time.RFC3339), - ScrapedAt: trivyDB.UpdatedAt.UTC(), + if lane != nil { + engineVer = append(engineVer, lane.EngineVersion()) + ruleset = append(ruleset, lane.RulesetVersion()) + lbOut, err = lane.Run(ctx, locator) + if err != nil { + half = record.HalfStatusFailed + incomplete("%v", err) + } else { + for _, p := range lbOut.Problems { + incomplete("%s", p) + } + res.Notes = append(res.Notes, lbOut.Notes...) + n := lbOut.Recall.Count + res.RecallCandidates = &n + } + if snapshot == nil { + // The SAST run names the corpus it read; with no advisory + // feed in this scan, that corpus is the rule pack. + snapshot = lane.Snapshot() + } } } - res.Emitted = len(emissions) + results := lanea.Results(emissions) + results = append(results, lbOut.Results...) + res.Emitted = len(results) - if len(emissions) > 0 { - if rec, err = ctl.Transition(rec, scanctl.FindingsEvent(record.HalfSast, lanea.Results(emissions)...)); err != nil { + if len(results) > 0 { + if rec, err = ctl.Transition(rec, scanctl.FindingsEvent(record.HalfSast, results...)); err != nil { return res, err } } @@ -246,8 +333,11 @@ func Run(ctx context.Context, req Request) (Result, error) { ConfigSource: polRef, Actor: actor, ResolvedAt: started, }, SastTool: record.ToolComponent{Name: "anvil", Version: req.AnvilVersion}, - SastResults: lanea.Results(emissions), + SastExtensions: lbOut.Extensions, + SastTaxonomies: lbOut.Taxonomies, + SastResults: results, AdvisorySnapshot: snapshot, + SpecHarvest: lbOut.SpecHarvest, }) if err != nil { return res, err @@ -264,14 +354,15 @@ func Run(ctx context.Context, req Request) (Result, error) { TargetKind: string(req.Kind), TargetLocator: locator, TriggerRef: req.Event, - RulesetVersion: RulesetVersion, - SastEngineVersion: engineVer, + RulesetVersion: strings.Join(ruleset, "+"), + SastEngineVersion: strings.Join(engineVer, "; "), AdvisorySnapshot: snapshotDigest(snapshot), FinishedAt: now().UTC(), Seal: seal, AuditVersion: rec.Version, Complete: res.Complete, - DetectorsRun: []record.DetectorKind{detector}, + DetectorsRun: detectorsRun(detected, lane, res.RecallCandidates), + RecallCandidates: res.RecallCandidates, Log: l, }) if err != nil { @@ -280,9 +371,9 @@ func Run(ctx context.Context, req Request) (Result, error) { res.Write = w switch { - case len(emissions) > 0: + case len(results) > 0: res.Outcome = OutcomeFindings - case half != record.HalfStatusSealed || !res.Complete: + case half != record.HalfStatusSealed || !res.Complete || len(res.NotRun) > 0: res.Outcome = OutcomeRefused default: res.Outcome = OutcomeClean @@ -290,22 +381,31 @@ func Run(ctx context.Context, req Request) (Result, error) { return res, nil } -// laneDetector is the one detector kind each Lane A scan produces. -func laneDetector(k Kind) (record.DetectorKind, error) { - switch k { - case KindRepo: - return record.DetectorKindSCA, nil - case KindHost: - return record.DetectorKindHost, nil +// detectorsRun is what the store may resolve findings for: every lane that +// ran, except a Lane B run that failed before it counted anything, whose +// absent findings prove nothing. +func detectorsRun(detected []record.DetectorKind, lane *laneb.Lane, count *int) []record.DetectorKind { + var out []record.DetectorKind + for _, d := range detected { + if d == record.DetectorKindSast && (lane == nil || count == nil) { + continue + } + out = append(out, d) } - return "", fmt.Errorf("scan: unknown scan kind %q", k) + return out +} + +// policyAllows reports whether the resolved rule lets a detector run. A rule +// that names no detectors allows every one. +func policyAllows(rule policy.ResolvedRule, d record.DetectorKind) bool { + return len(rule.Detectors) == 0 || rule.HasDetector(d) } // resolvePolicy evaluates the trigger policy for this scan. A repository with -// no policy file runs a manual scan with every Lane A detector; a scheduled +// no policy file runs a manual scan with every enabled detector; a scheduled // scan runs only when a rule matched, because "a timer fired" is not on its // own a reason to scan. -func resolvePolicy(req Request, detector record.DetectorKind) (policy.ResolvedRule, string, error) { +func resolvePolicy(req Request) (policy.ResolvedRule, string, error) { path := req.PolicyPath if path == "" && req.Kind == KindRepo { p, err := policy.Locate(req.RepoPath) @@ -334,10 +434,6 @@ func resolvePolicy(req Request, detector record.DetectorKind) (policy.ResolvedRu if (req.Full || req.Event == "schedule") && len(rule.Matched) == 0 { return rule, path, fmt.Errorf("%w: no rule in %s matches event %q", ErrPolicyRefused, path, req.Event) } - if len(rule.Detectors) > 0 && !rule.HasDetector(detector) { - return rule, path, fmt.Errorf("%w: the resolved rule's detectors are %v, which exclude %q", - ErrPolicyRefused, rule.Detectors, detector) - } return rule, path, nil } diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 9b3c067..aca4e3a 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -33,11 +33,32 @@ type Settings struct { // TrivyDB admits findings decided by Trivy's own database, with tier-2 // attribution. False unless the file says true. TrivyDB bool + // Recall is Lane B's recall tier. + Recall Recall +} + +// Recall configures Lane B's recall tier: the rule pack and the three tools +// it runs as subprocesses. It is on unless the file says enabled: false. Its +// rules are MIT and Apache-2.0 (data/rules/MANIFEST.json), so nothing like +// the Trivy database's licence question keeps it off by default; an absent +// rule pack or tool makes a repository scan exit as a missing tool, never +// clean. +type Recall struct { + Enabled bool + // Rules is the rule pack directory. + Rules string + // Opengrep, Gosec and Bandit are the executables; empty means the tool's + // name on PATH. + Opengrep, Gosec, Bandit string + // GoBin is the directory holding the go command gosec loads packages + // with; empty means the go on PATH. + GoBin string } // Default is an installation with nothing configured but a state directory. func Default() Settings { - return Settings{StateDir: "/var/lib/anvil", SpoolDir: "/var/lib/anvil/dispatch", MirrorRoot: "/usr/share/anvil"} + return Settings{StateDir: "/var/lib/anvil", SpoolDir: "/var/lib/anvil/dispatch", MirrorRoot: "/usr/share/anvil", + Recall: Recall{Enabled: true, Rules: "/usr/share/anvil/rules"}} } // StorePath and CachePath are the two databases under StateDir. @@ -49,7 +70,9 @@ func (s Settings) CachePath() string { return filepath.Join(s.StateDir, "anvil-c // ErrSettings reports an unreadable or invalid configuration file. var ErrSettings = errors.New("settings: invalid configuration") -var keys = map[string]bool{"version": true, "stateDir": true, "feeds": true, "policy": true, "spoolDir": true, "mirrorRoot": true, "trivyDB": true} +var keys = map[string]bool{"version": true, "stateDir": true, "feeds": true, "policy": true, "spoolDir": true, "mirrorRoot": true, "trivyDB": true, "recall": true} + +var recallKeys = map[string]bool{"enabled": true, "rules": true, "opengrep": true, "gosec": true, "bandit": true, "goBin": true} // Load reads a configuration file. A missing file at DefaultPath is the // default configuration; a missing file anywhere else was named on purpose and @@ -131,5 +154,38 @@ func Load(path string) (Settings, error) { } s.TrivyDB = b } + if v, present := m["recall"]; present { + mm, ok := v.(map[string]any) + if !ok { + return s, fmt.Errorf("%w: %s: recall must be a mapping", ErrSettings, path) + } + for k := range mm { + if !recallKeys[k] { + return s, fmt.Errorf("%w: %s: recall has an unknown key %q", ErrSettings, path, k) + } + } + if e, present := mm["enabled"]; present { + b, ok := e.(bool) + if !ok { + return s, fmt.Errorf("%w: %s: recall.enabled must be true or false", ErrSettings, path) + } + s.Recall.Enabled = b + } + for key, dst := range map[string]*string{"rules": &s.Recall.Rules, "opengrep": &s.Recall.Opengrep, + "gosec": &s.Recall.Gosec, "bandit": &s.Recall.Bandit, "goBin": &s.Recall.GoBin} { + v, present := mm[key] + if !present || v == nil { + continue + } + sv, ok := v.(string) + if !ok || sv == "" { + return s, fmt.Errorf("%w: %s: recall.%s must be a non-empty path", ErrSettings, path, key) + } + if !filepath.IsAbs(sv) { + sv = filepath.Join(base, sv) + } + *dst = sv + } + } return s, nil } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 50d23ce..d0a5f00 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -33,6 +33,28 @@ func TestTheTrivyDatabaseIsOffUnlessTheFileTurnsItOn(t *testing.T) { } } +func TestLaneBIsOnUnlessTheFileTurnsItOff(t *testing.T) { + s, err := Load(write(t, "version: 1\n")) + if err != nil { + t.Fatal(err) + } + if !s.Recall.Enabled || s.Recall.Rules != "/usr/share/anvil/rules" { + t.Fatalf("default recall settings %+v", s.Recall) + } + p := write(t, "version: 1\nrecall:\n rules: data/rules\n opengrep: /opt/opengrep\n") + s, err = Load(p) + if err != nil { + t.Fatal(err) + } + if !s.Recall.Enabled || s.Recall.Rules != filepath.Join(filepath.Dir(p), "data/rules") || s.Recall.Opengrep != "/opt/opengrep" { + t.Fatalf("recall settings %+v", s.Recall) + } + off, err := Load(write(t, "version: 1\nrecall:\n enabled: false\n")) + if err != nil || off.Recall.Enabled { + t.Fatalf("recall.enabled: false -> %+v, %v", off.Recall, err) + } +} + func TestLoadRefuses(t *testing.T) { for name, body := range map[string]string{ "unknown key": "version: 1\ntrivyDb:\n enabled: true\n", @@ -41,6 +63,10 @@ func TestLoadRefuses(t *testing.T) { "no version": "stateDir: /x\n", "wrong version": "version: 2\n", "empty path": "version: 1\nfeeds: \"\"\n", + "unknown recall key": "version: 1\nrecall:\n rule: x\n", + "recall not a map": "version: 1\nrecall: on\n", + "recall string bool": "version: 1\nrecall:\n enabled: \"false\"\n", + "recall empty path": "version: 1\nrecall:\n gosec: \"\"\n", } { if _, err := Load(write(t, body)); !errors.Is(err, ErrSettings) { t.Errorf("%s: err = %v, want ErrSettings", name, err) diff --git a/internal/store/migrate_test.go b/internal/store/migrate_test.go index 4e01212..8807feb 100644 --- a/internal/store/migrate_test.go +++ b/internal/store/migrate_test.go @@ -182,44 +182,67 @@ func TestIncludeExpansion(t *testing.T) { // TestMigrateBuildsExactlyTheStoreSchema is the packet's schema-equivalence evidence // item: a database built by the migration runner and a database built by -// applying schema.sql directly must be indistinguishable, object for object. +// applying schema.sql directly, then each later migration's SQL in order, must +// be indistinguishable, object for object. func TestMigrateBuildsExactlyTheStoreSchema(t *testing.T) { + migrations, err := Migrations() + if err != nil { + t.Fatalf("Migrations: %v", err) + } + n := len(migrations) migrated := openMemory(t) applied, err := Migrate(context.Background(), migrated, "") if err != nil { t.Fatalf("Migrate: %v", err) } - if len(applied) != 1 || applied[0] != 1 { - t.Fatalf("Migrate applied %v, want [1]", applied) + if len(applied) != n || applied[0] != 1 || applied[n-1] != n { + t.Fatalf("Migrate applied %v, want 1 to %d", applied, n) } direct := openMemory(t) if _, err := direct.Exec(Schema()); err != nil { t.Fatalf("applying schema.sql directly: %v", err) } + for _, m := range migrations[1:] { + if _, err := direct.Exec(m.SQL); err != nil { + t.Fatalf("applying %s directly: %v", m.Filename, err) + } + } if got, want := schemaObjects(t, migrated), schemaObjects(t, direct); got != want { t.Fatalf("migrated schema differs from schema.sql applied directly:\n--- migrated ---\n%s\n--- direct ---\n%s", got, want) } - if v := mustSchemaVersion(t, migrated); v != 1 { - t.Fatalf("PRAGMA user_version = %d after migrating, want 1", v) + if v := mustSchemaVersion(t, migrated); v != n { + t.Fatalf("PRAGMA user_version = %d after migrating, want %d", v, n) } rows := ledgerRows(t, migrated) - if len(rows) != 1 { - t.Fatalf("schema_migration has %d rows, want 1", len(rows)) + if len(rows) != n { + t.Fatalf("schema_migration has %d rows, want %d", len(rows), n) } - migrations, err := Migrations() - if err != nil { - t.Fatalf("Migrations: %v", err) + for i, m := range migrations { + if rows[i].Version != m.Version || rows[i].Name != m.Name || rows[i].Checksum != m.Checksum { + t.Fatalf("ledger row %+v does not describe %s", rows[i], m.Filename) + } + if rows[i].AppliedAt == "" { + t.Fatalf("ledger row %d has no applied_at timestamp", i) + } } - if rows[0].Version != 1 || rows[0].Name != "init" || rows[0].Checksum != migrations[0].Checksum { - t.Fatalf("ledger row %+v does not describe %s", rows[0], migrations[0].Filename) + if rows[0].Name != "init" { + t.Fatalf("migration 1 is %q, want init", rows[0].Name) } - if rows[0].AppliedAt == "" { - t.Fatal("ledger row has no applied_at timestamp") +} + +// latestReal is the number of embedded migrations; synthetic test migrations +// are numbered after it. +func latestReal(t *testing.T) int { + t.Helper() + n, err := LatestVersion() + if err != nil { + t.Fatalf("LatestVersion: %v", err) } + return n } func TestMigrateIsANoOpWhenCurrent(t *testing.T) { @@ -247,39 +270,45 @@ func TestMigrateIsANoOpWhenCurrent(t *testing.T) { } } -// TestMigrateAppliesInNumberedOrderInsideTransactions injects later versions, -// which is the only way to test ordering and transactionality while 0001 is -// the only real migration. The alternative is finding out during the first -// upgrade a user ever performs. +// TestMigrateAppliesInNumberedOrderInsideTransactions injects later versions +// after the real ones, so ordering and transactionality are tested past the +// last migration that exists. The alternative is finding out during the next +// upgrade a user performs. func TestMigrateAppliesInNumberedOrderInsideTransactions(t *testing.T) { db, dir := openOnDisk(t) ctx := context.Background() + n := latestReal(t) migrations := withSynthetic(t, - synthetic(2, "second", "CREATE TABLE second_step (x INTEGER);"), - synthetic(3, "third", "CREATE TABLE third_step (y INTEGER REFERENCES second_step(x));"), + synthetic(n+1, "second", "CREATE TABLE second_step (x INTEGER);"), + synthetic(n+2, "third", "CREATE TABLE third_step (y INTEGER REFERENCES second_step(x));"), ) applied, err := migrateWith(ctx, db, migrations, dir) if err != nil { t.Fatalf("migrateWith: %v", err) } - if len(applied) != 3 || applied[0] != 1 || applied[1] != 2 || applied[2] != 3 { - t.Fatalf("applied %v, want [1 2 3] in order", applied) + if len(applied) != n+2 { + t.Fatalf("applied %v, want 1 to %d", applied, n+2) + } + for i, v := range applied { + if v != i+1 { + t.Fatalf("applied %v, want 1 to %d in order", applied, n+2) + } } - if v := mustSchemaVersion(t, db); v != 3 { - t.Fatalf("user_version = %d, want 3", v) + if v := mustSchemaVersion(t, db); v != n+2 { + t.Fatalf("user_version = %d, want %d", v, n+2) } - if rows := ledgerRows(t, db); len(rows) != 3 { - t.Fatalf("ledger has %d rows, want 3", len(rows)) + if rows := ledgerRows(t, db); len(rows) != n+2 { + t.Fatalf("ledger has %d rows, want %d", len(rows), n+2) } // third_step references second_step; it could not have been created first. - var n int - if err := db.QueryRow(`SELECT count(*) FROM sqlite_schema WHERE name IN ('second_step','third_step')`).Scan(&n); err != nil { + var tables int + if err := db.QueryRow(`SELECT count(*) FROM sqlite_schema WHERE name IN ('second_step','third_step')`).Scan(&tables); err != nil { t.Fatalf("counting new tables: %v", err) } - if n != 2 { - t.Fatalf("expected both synthetic tables, found %d", n) + if tables != 2 { + t.Fatalf("expected both synthetic tables, found %d", tables) } } @@ -288,28 +317,29 @@ func TestMigrateAppliesInNumberedOrderInsideTransactions(t *testing.T) { func TestFailedMigrationLeavesSchemaUntouched(t *testing.T) { db := openMemory(t) ctx := context.Background() + n := latestReal(t) - migrations := withSynthetic(t, synthetic(2, "broken", + migrations := withSynthetic(t, synthetic(n+1, "broken", "CREATE TABLE half_applied (x INTEGER);\nCREATE TABLE half_applied (x INTEGER);")) applied, err := migrateWith(ctx, db, migrations, t.TempDir()) if err == nil { t.Fatal("a migration with invalid DDL was accepted") } - if len(applied) != 1 || applied[0] != 1 { - t.Fatalf("applied %v, want [1] — 0001 succeeded and 0002 must not count", applied) + if len(applied) != n || applied[n-1] != n { + t.Fatalf("applied %v, want 1 to %d: the real migrations succeeded and the broken one must not count", applied, n) } - if v := mustSchemaVersion(t, db); v != 1 { - t.Fatalf("user_version = %d after a failed migration, want 1", v) + if v := mustSchemaVersion(t, db); v != n { + t.Fatalf("user_version = %d after a failed migration, want %d", v, n) } - if rows := ledgerRows(t, db); len(rows) != 1 { - t.Fatalf("ledger has %d rows after a failed migration, want 1", len(rows)) + if rows := ledgerRows(t, db); len(rows) != n { + t.Fatalf("ledger has %d rows after a failed migration, want %d", len(rows), n) } - var n int - if err := db.QueryRow(`SELECT count(*) FROM sqlite_schema WHERE name = 'half_applied'`).Scan(&n); err != nil { + var left int + if err := db.QueryRow(`SELECT count(*) FROM sqlite_schema WHERE name = 'half_applied'`).Scan(&left); err != nil { t.Fatalf("looking for the rolled-back table: %v", err) } - if n != 0 { + if left != 0 { t.Fatal("half_applied survived a rolled-back migration") } } @@ -356,12 +386,13 @@ func TestMigrateRefusesADowngrade(t *testing.T) { t.Fatalf("Migrate: %v", err) } // Simulate an older binary opening a store a newer one migrated. + future := latestReal(t) + 1 if _, err := db.Exec( - `INSERT INTO schema_migration (version, name, checksum, applied_at) VALUES (2, 'future', 'x', '2026-01-01T00:00:00Z')`, + `INSERT INTO schema_migration (version, name, checksum, applied_at) VALUES (?, 'future', 'x', '2026-01-01T00:00:00Z')`, future, ); err != nil { t.Fatalf("seeding a future migration: %v", err) } - if _, err := db.Exec(`PRAGMA user_version = 2`); err != nil { + if _, err := db.Exec(fmt.Sprintf(`PRAGMA user_version = %d`, future)); err != nil { t.Fatalf("bumping user_version: %v", err) } @@ -402,14 +433,15 @@ func TestMigrateRefusesAPopulatedDatabaseWithNoLedger(t *testing.T) { func TestMigrateRefusesAGapInTheLedger(t *testing.T) { db := openMemory(t) ctx := context.Background() + n := latestReal(t) migrations := withSynthetic(t, - synthetic(2, "second", "CREATE TABLE second_step (x INTEGER);"), - synthetic(3, "third", "CREATE TABLE third_step (y INTEGER);"), + synthetic(n+1, "second", "CREATE TABLE second_step (x INTEGER);"), + synthetic(n+2, "third", "CREATE TABLE third_step (y INTEGER);"), ) if _, err := migrateWith(ctx, db, migrations, t.TempDir()); err != nil { t.Fatalf("migrateWith: %v", err) } - if _, err := db.Exec(`DELETE FROM schema_migration WHERE version = 2`); err != nil { + if _, err := db.Exec(`DELETE FROM schema_migration WHERE version = ?`, n+1); err != nil { t.Fatalf("punching a hole in the ledger: %v", err) } if _, err := migrateWith(ctx, db, migrations, t.TempDir()); !errors.Is(err, ErrMigrationLedger) { @@ -428,7 +460,8 @@ func TestMigrateRefusesToUpgradeWithoutASnapshotDirectory(t *testing.T) { t.Fatalf("initial Migrate: %v", err) } - migrations := withSynthetic(t, synthetic(2, "second", "CREATE TABLE second_step (x INTEGER);")) + n := latestReal(t) + migrations := withSynthetic(t, synthetic(n+1, "second", "CREATE TABLE second_step (x INTEGER);")) _, err := migrateWith(ctx, db, migrations, "") if !errors.Is(err, ErrSnapshotRequired) { t.Fatalf("Migrate upgraded a populated database with no snapshot directory, got %v", err) @@ -436,7 +469,7 @@ func TestMigrateRefusesToUpgradeWithoutASnapshotDirectory(t *testing.T) { if !strings.Contains(err.Error(), "forward-only") { t.Errorf("error does not explain why the snapshot is mandatory: %v", err) } - if v := mustSchemaVersion(t, db); v != 1 { + if v := mustSchemaVersion(t, db); v != n { t.Fatalf("a refused upgrade changed user_version to %d", v) } } @@ -453,12 +486,13 @@ func TestSnapshotIsTakenBeforeAnUpgrade(t *testing.T) { } snapDir := filepath.Join(dir, "snapshots") - migrations := withSynthetic(t, synthetic(2, "second", "CREATE TABLE second_step (x INTEGER);")) + n := latestReal(t) + migrations := withSynthetic(t, synthetic(n+1, "second", "CREATE TABLE second_step (x INTEGER);")) if _, err := migrateWith(ctx, db, migrations, snapDir); err != nil { t.Fatalf("migrateWith: %v", err) } - snap := filepath.Join(snapDir, "anvil-pre-v1.db") + snap := filepath.Join(snapDir, fmt.Sprintf("anvil-pre-v%d.db", n)) if _, err := os.Stat(snap); err != nil { t.Fatalf("no pre-migration snapshot at %s: %v", snap, err) } @@ -470,14 +504,14 @@ func TestSnapshotIsTakenBeforeAnUpgrade(t *testing.T) { defer func() { _ = restored.Close() }() restored.SetMaxOpenConns(1) - if v := mustSchemaVersion(t, restored); v != 1 { - t.Fatalf("snapshot is at user_version %d, want the pre-upgrade version 1", v) + if v := mustSchemaVersion(t, restored); v != n { + t.Fatalf("snapshot is at user_version %d, want the pre-upgrade version %d", v, n) } - var n int - if err := restored.QueryRow(`SELECT count(*) FROM sqlite_schema WHERE name = 'second_step'`).Scan(&n); err != nil { + var found int + if err := restored.QueryRow(`SELECT count(*) FROM sqlite_schema WHERE name = 'second_step'`).Scan(&found); err != nil { t.Fatalf("querying the snapshot: %v", err) } - if n != 0 { + if found != 0 { t.Fatal("the snapshot contains the migration it was supposed to precede") } } diff --git a/internal/store/migrations/0002_recall_candidates.sql b/internal/store/migrations/0002_recall_candidates.sql new file mode 100644 index 0000000..a2a77e9 --- /dev/null +++ b/internal/store/migrations/0002_recall_candidates.sql @@ -0,0 +1,18 @@ +-- 0002_recall_candidates — Lane B's candidates-per-scan metric (Phase 6, 2026-10-03). +-- +-- Candidates per scan, not model size, decides whether Lane B is affordable +-- (eval/register.yaml, the candidates-per-scan row), and the plan makes it a +-- permanent per-scan metric rather than a number measured once. The control +-- plane writes it on the scan_run row of every scan. +-- +-- NULL means Lane B did not run in this scan (a host scan, or a repository +-- scan whose policy or configuration left it out). 0 means it ran and its +-- rules matched nothing. The two are different statements and the column +-- keeps them apart; a negative count is refused. +-- +-- Record v1 is frozen, so this is an additive, numbered migration and not an +-- edit to schema.sql: a store migrated to version 2 and a fresh one built from +-- 0001 then 0002 have the same shape. + +ALTER TABLE scan_run ADD COLUMN recall_candidates INTEGER + CONSTRAINT ck_scan_run_recall_candidates CHECK (recall_candidates IS NULL OR recall_candidates >= 0); diff --git a/internal/store/recall_candidates_test.go b/internal/store/recall_candidates_test.go new file mode 100644 index 0000000..eac7481 --- /dev/null +++ b/internal/store/recall_candidates_test.go @@ -0,0 +1,39 @@ +package store + +import ( + "context" + "testing" +) + +// TestRecallCandidatesKeepsNotRunApartFromNone holds migration 0002: NULL is +// "Lane B did not run", 0 is "it ran and matched nothing", and a negative +// count is refused by the column's CHECK. +func TestRecallCandidatesKeepsNotRunApartFromNone(t *testing.T) { + db := openMemory(t) + if _, err := Migrate(context.Background(), db, ""); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`INSERT INTO target (target_id, kind, locator) VALUES (1, 'repo', 'repo:x')`); err != nil { + t.Fatal(err) + } + insert := func(v any) error { + _, err := db.Exec(`INSERT INTO scan_run (target_id, started_at, status, ruleset_version, recall_candidates) +VALUES (1, '2026-10-03T00:00:00Z', 'ok', 'r', ?)`, v) + return err + } + for _, ok := range []any{nil, 0, 225} { + if err := insert(ok); err != nil { + t.Errorf("recall_candidates %v refused: %v", ok, err) + } + } + if err := insert(-1); err == nil { + t.Error("a negative candidate count was stored") + } + var nulls, zeros int + if err := db.QueryRow(`SELECT sum(recall_candidates IS NULL), sum(recall_candidates = 0) FROM scan_run`).Scan(&nulls, &zeros); err != nil { + t.Fatal(err) + } + if nulls != 1 || zeros != 1 { + t.Fatalf("NULL and 0 did not stay apart: %d NULL, %d zero", nulls, zeros) + } +} diff --git a/internal/store/scanwrite.go b/internal/store/scanwrite.go index 9bb1db3..174927b 100644 --- a/internal/store/scanwrite.go +++ b/internal/store/scanwrite.go @@ -84,6 +84,11 @@ type ScanWrite struct { // these kinds can be resolved by it. DetectorsRun []record.DetectorKind + // RecallCandidates is Lane B's candidates-per-scan count, written to + // scan_run.recall_candidates (migration 0002). Nil means Lane B did not + // run; a pointer to 0 means it ran and matched nothing. + RecallCandidates *int + // Log is the assembled record. Its SAST results become the findings, and // its canonical JSON becomes audit_record.payload. Log *record.SARIFLog @@ -193,6 +198,8 @@ func (w ScanWrite) check() error { return errors.New("store: a scan needs its assembled record") case w.Seal.AuditID == "" || w.Seal.AuditID != w.Log.Properties.AuditID: return fmt.Errorf("store: the seal (%q) and the record (%q) are different audits", w.Seal.AuditID, w.Log.Properties.AuditID) + case w.RecallCandidates != nil && *w.RecallCandidates < 0: + return fmt.Errorf("store: a candidate count of %d is not a count", *w.RecallCandidates) case !record.IsTerminalHalfStatus(w.Seal.Sast.Status): return fmt.Errorf("store: the SAST half is %q; a scan is written after its half reaches a terminal status", w.Seal.Sast.Status) } @@ -326,6 +333,13 @@ func nullString(s string) any { return s } +func nullInt(p *int) any { + if p == nil { + return nil + } + return *p +} + // writer is one write transaction's statements. type writer struct { ctx context.Context @@ -359,10 +373,10 @@ func (t *writer) target(kind, locator string) (int64, error) { func (t *writer) scanRun(targetID int64, w ScanWrite, status, rollupHash string) (int64, error) { r, err := t.c.ExecContext(t.ctx, ` INSERT INTO scan_run (target_id, trigger_ref, commit_sha, started_at, finished_at, status, - sast_engine_ver, ruleset_version, advisory_snapshot, rollup_hash) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + sast_engine_ver, ruleset_version, advisory_snapshot, rollup_hash, recall_candidates) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, targetID, nullString(w.TriggerRef), nullString(w.CommitSHA), ts(w.Seal.StartedAt), t.now, status, - nullString(w.SastEngineVersion), w.RulesetVersion, nullString(w.AdvisorySnapshot), rollupHash) + nullString(w.SastEngineVersion), w.RulesetVersion, nullString(w.AdvisorySnapshot), rollupHash, nullInt(w.RecallCandidates)) if err != nil { return 0, fmt.Errorf("store: recording scan run: %w", err) } diff --git a/schemas/anvil-record-v1.schema.json b/schemas/anvil-record-v1.schema.json index 847388e..d633811 100644 --- a/schemas/anvil-record-v1.schema.json +++ b/schemas/anvil-record-v1.schema.json @@ -6,7 +6,7 @@ "x-anvil-baseSchema": "https://json.schemastore.org/sarif-2.1.0.json", "x-anvil-validationProcedure": "TWO GATES, both required. Gate 1: validate the record against https://json.schemastore.org/sarif-2.1.0.json (stock SARIF conformance; needed for GitHub and DefectDojo to accept it). Gate 2: validate against THIS schema (the anvil/* extension and the S6-required fields). This schema does not $ref the SARIF base schema because that reference is not resolvable in Anvil's offline CI; keeping the two gates separate makes the offline gate work and makes a SARIF-conformance failure distinguishable from an anvil/* failure.", - "x-anvil-contractVersion": "1.0.0", + "x-anvil-contractVersion": "1.1.0", "type": "object", "required": ["$schema", "version", "runs", "properties"], @@ -120,6 +120,44 @@ "type": "string", "enum": ["static_only", "requires_dynamic_confirmation"] }, + "nonBlank": { "type": "string", "pattern": "\\S" }, + + "ruleProvenance": { + "description": "Record 1.1.0, anvil/ruleProvenance: where a rule came from, stated in full or not at all.", + "type": "object", + "required": ["source", "repository", "version", "rulePath", "licenseSpdx", "licenseEvidence"], + "properties": { + "source": { "$ref": "#/$defs/nonBlank" }, + "repository": { "$ref": "#/$defs/nonBlank" }, + "version": { "$ref": "#/$defs/nonBlank" }, + "rulePath": { "$ref": "#/$defs/nonBlank" }, + "licenseSpdx": { "$ref": "#/$defs/nonBlank" }, + "licenseEvidence": { "$ref": "#/$defs/nonBlank" } + } + }, + + "toolExtension": { + "type": "object", + "required": ["name"], + "properties": { + "name": { "$ref": "#/$defs/nonBlank" }, + "rules": { + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "properties": { + "type": "object", + "properties": { "anvil/ruleProvenance": { "$ref": "#/$defs/ruleProvenance" } } + } + } + } + } + } + }, + "trustedString": { "description": "An anvil/* extension string that originated outside Anvil, carrying its trust inline. SARIF-native strings cannot change shape, so those are classified by JSON Pointer in anvil/trust.fields instead.", "type": "object", @@ -252,7 +290,13 @@ "type": "object", "required": ["tool", "automationDetails", "results", "properties"], "properties": { - "tool": { "type": "object" }, + "tool": { + "description": "SARIF tool. Record 1.1.0 constrains tool.extensions: the rule corpora and native analysers Lane B ran, each holding the rules its results cite, each rule stating anvil/ruleProvenance.", + "type": "object", + "properties": { + "extensions": { "type": "array", "items": { "$ref": "#/$defs/toolExtension" } } + } + }, "automationDetails": { "type": "object", "required": ["correlationGuid"], @@ -499,6 +543,19 @@ "relatedLocations": { "type": "array" }, "codeFlows": { "type": "array" }, "taxa": { "type": "array" }, + "rule": { + "description": "Record 1.1.0: the rule this result cites, by id and by its tool extension's index. internal/record checks that it resolves; this schema checks its shape.", + "type": "object", + "required": ["id", "toolComponent"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "toolComponent": { + "type": "object", + "required": ["index"], + "properties": { "index": { "type": "integer", "minimum": 0 } } + } + } + }, "webRequest": { "type": "object" }, "webResponse": { "type": "object" }, "provenance": { "type": "object" }, diff --git a/test/e2e/fixture.sh b/test/e2e/fixture.sh index dc33171..fb7742c 100644 --- a/test/e2e/fixture.sh +++ b/test/e2e/fixture.sh @@ -17,7 +17,9 @@ fixture="$root/testdata/lanea-fixture" go build -o "$work/anvil" "$root/cmd/anvil" anvil="$work/anvil" export ANVIL_CONFIG="$work/anvil.yml" -printf 'version: 1\nstateDir: %s/state\ntrivyDB:\n enabled: true\n' "$work" > "$ANVIL_CONFIG" +# Lane B is on by default; the Lane A fixture repository holds no source in a +# language its rules cover, so it needs the rule pack and no tool. +printf 'version: 1\nstateDir: %s/state\ntrivyDB:\n enabled: true\nrecall:\n rules: %s/data/rules\n' "$work" "$root" > "$ANVIL_CONFIG" expect() { # expect STATUS DESCRIPTION -- COMMAND... want=$1; what=$2; shift 3 diff --git a/test/e2e/laneb.sh b/test/e2e/laneb.sh new file mode 100644 index 0000000..8220e6f --- /dev/null +++ b/test/e2e/laneb.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# Lane B end to end, on testdata/laneb-fixture, through the built binary and the +# real, pinned tools. +# +# sh test/e2e/laneb.sh +# +# Needs Go, Python with jsonschema (eval/'s dependencies), and opengrep 1.26.0, +# gosec 2.29.0 and bandit 1.9.4, on PATH or named by ANVIL_OPENGREP, +# ANVIL_GOSEC and ANVIL_BANDIT. CI runs it in the "Lane B end to end (real +# tools)" job. +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +fixture="$root/testdata/laneb-fixture" + +go build -o "$work/anvil" "$root/cmd/anvil" +anvil="$work/anvil" +export ANVIL_CONFIG="$work/anvil.yml" +{ + printf 'version: 1\nstateDir: %s/state\nrecall:\n rules: %s/data/rules\n' "$work" "$root" + [ -n "${ANVIL_OPENGREP:-}" ] && printf ' opengrep: %s\n' "$ANVIL_OPENGREP" + [ -n "${ANVIL_GOSEC:-}" ] && printf ' gosec: %s\n' "$ANVIL_GOSEC" + [ -n "${ANVIL_BANDIT:-}" ] && printf ' bandit: %s\n' "$ANVIL_BANDIT" + printf ' goBin: %s\n' "$(dirname "$(command -v go)")" +} > "$ANVIL_CONFIG" + +expect() { # expect STATUS DESCRIPTION -- COMMAND... + want=$1; what=$2; shift 3 + set +e + "$@" > "$work/out" 2> "$work/err" + got=$? + set -e + cat "$work/out" + if [ "$got" -ne "$want" ]; then + echo "FAIL: $what exited $got, want $want" >&2 + cat "$work/err" >&2 + exit 1 + fi + cat "$work/err" + echo "ok: $what (exit $got)" +} + +expect 1 "Lane B scan, first run" -- "$anvil" scan --repo "$fixture" --target laneb-fixture --out "$work/laneb1.sarif" +grep -q 'findings, 12 finding(s); scan_run 1 ok; new 12, persisting 0, regressed 0, fixed 0' "$work/out" || + { echo "FAIL: first run marks" >&2; exit 1; } +expect 1 "Lane B scan, second run" -- "$anvil" scan --repo "$fixture" --target laneb-fixture --out "$work/laneb2.sarif" +grep -q 'new 0, persisting 12, regressed 0, fixed 0' "$work/out" || { echo "FAIL: second run marks" >&2; exit 1; } +expect 1 "anvil recall" -- "$anvil" recall "$fixture" +grep -q '"count": 12' "$work/out" || { echo "FAIL: anvil recall count" >&2; exit 1; } + +python3 - "$work/laneb1.sarif" <<'PY' +import json, sys +log = json.load(open(sys.argv[1])) +run = log["runs"][0] +assert log["properties"]["anvil/schemaVersion"] == "1.1.0", log["properties"]["anvil/schemaVersion"] +assert len(run["results"]) == 12 +for r in run["results"]: + p = r["properties"] + assert p["anvil/verdict"] == "unconfirmed" and p["anvil/confidence"] == 1, r["ruleId"] + assert p["anvil/detector"]["model"] == "" and "rank" not in r, r["ruleId"] + ext = run["tool"]["extensions"][r["rule"]["toolComponent"]["index"]] + prov = next(d for d in ext["rules"] if d["id"] == r["ruleId"])["properties"]["anvil/ruleProvenance"] + assert all(str(v).strip() for v in prov.values()), prov +assert [f["location"]["uri"] for f in run["properties"]["anvil/specHarvest"]["files"]] == ["api/openapi.yaml"] +print("ok: every result is unconfirmed, with complete rule provenance; the spec was harvested") +PY +python3 "$root/eval/tools/validate_record.py" "$root/schemas/anvil-record-v1.schema.json" \ + "$work/laneb1.sarif" "$work/laneb2.sarif" +echo "PASS: Lane B end to end on the fixture" diff --git a/testdata/laneb-fixture/.bandit b/testdata/laneb-fixture/.bandit new file mode 100644 index 0000000..2912285 --- /dev/null +++ b/testdata/laneb-fixture/.bandit @@ -0,0 +1,3 @@ +[bandit] +exclude: src +skips: B602,B307 diff --git a/testdata/laneb-fixture/.semgrepignore b/testdata/laneb-fixture/.semgrepignore new file mode 100644 index 0000000..8eba6c8 --- /dev/null +++ b/testdata/laneb-fixture/.semgrepignore @@ -0,0 +1 @@ +src/ diff --git a/testdata/laneb-fixture/README.md b/testdata/laneb-fixture/README.md new file mode 100644 index 0000000..9e4dac7 --- /dev/null +++ b/testdata/laneb-fixture/README.md @@ -0,0 +1,17 @@ +# Lane B fixture + +A repository Lane B scans in tests and in CI's Lane B end-to-end job. Every file is planted; nothing here +is compiled or run. What each file is for: + +| File | Expected | +|---|---| +| `src/app.py` | an `eval` of input (GitLab's Python rules) and a `shell=True` subprocess (bandit B602) | +| `src/suppressed.py` | the same `eval`, carrying `# nosec` and `# nosemgrep`: still reported, because a scanned repository does not get to switch Lane B off | +| `src/main.go`, `go.mod` | a command built from input (gosec G204) | +| `src/copy.c` | a fixed stack buffer filled from input (0xdea's C rules) | +| `src/Digest.java` | DES (GitLab's Java rules that remain after the LGPL-3.0-derived ones were excluded) | +| `web/app.js` | a `require` of a variable (GitLab's JavaScript rules) | +| `src/clean.py` | nothing | +| `tests/test_planted.py` | an `eval` in a test tree: excluded by the selection's paths | +| `.semgrepignore`, `.bandit` | each tries to hide `src/`; neither may have any effect | +| `api/openapi.yaml` | harvested onto the SAST run for the dynamic tier | diff --git a/testdata/laneb-fixture/api/openapi.yaml b/testdata/laneb-fixture/api/openapi.yaml new file mode 100644 index 0000000..e0ffe3d --- /dev/null +++ b/testdata/laneb-fixture/api/openapi.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.3 +info: + title: Lane B fixture + version: "1" +paths: + /items/{id}: + get: + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: an item diff --git a/testdata/laneb-fixture/go.mod b/testdata/laneb-fixture/go.mod new file mode 100644 index 0000000..228c6da --- /dev/null +++ b/testdata/laneb-fixture/go.mod @@ -0,0 +1,3 @@ +module example.invalid/lanebfixture + +go 1.22 diff --git a/testdata/laneb-fixture/src/Digest.java b/testdata/laneb-fixture/src/Digest.java new file mode 100644 index 0000000..f924473 --- /dev/null +++ b/testdata/laneb-fixture/src/Digest.java @@ -0,0 +1,8 @@ +// Planted for Lane B's fixture. Never compiled. +import javax.crypto.Cipher; + +public class Digest { + Cipher cipher() throws Exception { + return Cipher.getInstance("DES/CBC/PKCS5Padding"); + } +} diff --git a/testdata/laneb-fixture/src/app.py b/testdata/laneb-fixture/src/app.py new file mode 100644 index 0000000..6ff61cc --- /dev/null +++ b/testdata/laneb-fixture/src/app.py @@ -0,0 +1,11 @@ +"""Planted for Lane B's fixture. Never run.""" +import subprocess +import sys + + +def run_expression(): + return eval(sys.argv[1]) + + +def run_command(cmd): + return subprocess.call(cmd, shell=True) diff --git a/testdata/laneb-fixture/src/clean.py b/testdata/laneb-fixture/src/clean.py new file mode 100644 index 0000000..6203b5d --- /dev/null +++ b/testdata/laneb-fixture/src/clean.py @@ -0,0 +1,5 @@ +"""Planted for Lane B's fixture: nothing to find.""" + + +def add(a, b): + return a + b diff --git a/testdata/laneb-fixture/src/copy.c b/testdata/laneb-fixture/src/copy.c new file mode 100644 index 0000000..33ae209 --- /dev/null +++ b/testdata/laneb-fixture/src/copy.c @@ -0,0 +1,7 @@ +/* Planted for Lane B's fixture: a fixed stack buffer filled from input. Never compiled. */ +#include + +void copy(const char *src) { + char buf[16]; + strcpy(buf, src); +} diff --git a/testdata/laneb-fixture/src/main.go b/testdata/laneb-fixture/src/main.go new file mode 100644 index 0000000..98d42c8 --- /dev/null +++ b/testdata/laneb-fixture/src/main.go @@ -0,0 +1,17 @@ +// Package main is planted for Lane B's fixture. Never built. +package main + +import ( + "os" + "os/exec" +) + +type runner struct{} + +func (runner) run(name string) error { + return exec.Command("sh", "-c", name).Run() +} + +func main() { + _ = runner{}.run(os.Args[1]) +} diff --git a/testdata/laneb-fixture/src/suppressed.py b/testdata/laneb-fixture/src/suppressed.py new file mode 100644 index 0000000..c70d4db --- /dev/null +++ b/testdata/laneb-fixture/src/suppressed.py @@ -0,0 +1,6 @@ +"""Planted for Lane B's fixture: the scanned code asks to be ignored. Never run.""" +import sys + + +def run_expression(): + return eval(sys.argv[1]) # nosec # nosemgrep diff --git a/testdata/laneb-fixture/tests/test_planted.py b/testdata/laneb-fixture/tests/test_planted.py new file mode 100644 index 0000000..c505ae1 --- /dev/null +++ b/testdata/laneb-fixture/tests/test_planted.py @@ -0,0 +1,6 @@ +"""Planted for Lane B's fixture: a test tree, excluded by the selection. Never run.""" +import sys + + +def test_expression(): + assert eval(sys.argv[1]) diff --git a/testdata/laneb-fixture/web/app.js b/testdata/laneb-fixture/web/app.js new file mode 100644 index 0000000..48e6b37 --- /dev/null +++ b/testdata/laneb-fixture/web/app.js @@ -0,0 +1,5 @@ +// Planted for Lane B's fixture. Never run. +function load(name) { + return require(name); +} +module.exports = load;