Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ jobs:
platform: win32-x64
- os: macos-15-intel
platform: darwin-x64
- os: macos-14
- os: macos-15
platform: darwin-arm64
- os: ubuntu-24.04-arm
platform: linux-arm64
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pin-engines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
fail-fast: false
matrix:
include:
- os: macos-14 # Apple Silicon
- os: macos-15 # Apple Silicon
platform: darwin-arm64
- os: macos-15-intel # Intel
platform: darwin-x64
Expand Down
1 change: 1 addition & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ paths = [
'''fixtures/vulnerable-app/src/config\.ts''',
'''evals/run-evals\.ts''',
'''fixtures/secret-rls-corpus/''',
'''fixtures/coverage-honesty-corpus/gitleaksignore-relative/config\.yml''',
]
218 changes: 218 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,11 @@ See the [V3 migration guide](docs/V3-REPOSITORY-TRUST-MIGRATION.md) for schema a
`detected_technologies` explains the bounded repository signals CodeInspectus saw.
`pack_coverage` separately reports how many registered native analyzers and rules actually ran.
A pack state of `ran` means those listed rules executed; it is not a claim of complete security
coverage for the named language or framework. `not_applicable` means the installed pack did not
match detected project technology; `not_run`, `partial`, and `unavailable` distinguish scanner
coverage for the named language or framework. Native packs use a lightweight lexer, not a full parser:
they are pattern checks for ordinary code and can be evaded by deliberately crafted code (for example a
call hidden from the lexer while the file still reads as parsed). When the SAST scanner runs,
Opengrep's full parser still scans the same files for generic sinks; a full parser replacement is
planned for 4.0. `not_applicable` means the installed pack did not match detected project technology; `not_run`, `partial`, and `unavailable` distinguish scanner
filtering from incomplete or failed execution. The Flutter pack runs only when bounded repository
signals identify Flutter; an ordinary Dart package does not activate it. Android and iOS packs
likewise require bounded platform-project evidence and report their own platform metadata.
Expand Down
4 changes: 2 additions & 2 deletions detection-db/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "CodeInspectus Detection Database",
"version": "1.19.0",
"date": "2026-08-13",
"version": "1.19.1",
"date": "2026-10-31",
"description": "CodeInspectus's curated first-party detections. Native packs cover bounded AI/framework checks plus selected reconciled SAST rules; separately downloaded external engines remain additive fallbacks and broader scanners.",
"engine_versions": {
"opengrep": "1.33.1",
Expand Down
40 changes: 40 additions & 0 deletions detection-db/opengrep-ignore/default.semgrepignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# CodeInspectus managed Opengrep ignore list. Opengrep reads this file instead of a scanned
# repository's .semgrepignore (SEMGREP_R2C_INTERNAL_EXPLICIT_SEMGREPIGNORE), so a repository cannot
# remove files from SAST. The patterns below are Opengrep 1.33.1's built-in default list, unchanged.
# At run time CodeInspectus appends the build-output folders of src/config.ts BUILD_DIRS, where file
# routing drops Opengrep findings anyway, so the two lists cannot drift apart.
# Attribution: the default list is copied from Opengrep's .semgrepignore template
# (https://github.com/opengrep/opengrep, LGPL-2.1).


# Git administrative folder or file
.git
.svn
.hg
_darcs
CVS

# Common large paths
build/
vendor/
dist/
*.min.js
.env/
.tox/

# package managers
node_modules/
.npm/
.yarn/
.venv/
_opam/
_build/
_cargo/
# note that PHP composer uses vendor/ and C++ conan uses build/
# .venv is used both by Go and Python

# Common test paths
test/
tests/
testsuite/
*_test.go
1 change: 0 additions & 1 deletion detection-db/opengrep-rules/security-baseline/crypto.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ rules:
confidence: LOW
patterns:
- pattern-either:
- pattern: $T = Math.random()
- pattern: const $V = Math.random()
- metavariable-pattern:
metavariable: $V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ rules:
- pattern-either:
- pattern: $FS.readFile($P + ..., ...)
- pattern: $FS.readFileSync($P + ...)
- pattern: $FS.readFile(`...${...}...`, ...)
- pattern: $FS.createReadStream($P + ...)
- metavariable-pattern:
metavariable: $P
Expand Down
2 changes: 0 additions & 2 deletions detection-db/opengrep-rules/security-baseline/ssrf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ rules:
patterns:
- pattern-either:
- pattern: fetch($U + ..., ...)
- pattern: fetch(`...${...}...`, ...)
- pattern: axios.get($U + ..., ...)
- pattern: axios.get(`...${...}...`, ...)
- pattern: $H.get($U + ..., ...)
- pattern: $H.request($U + ..., ...)
- metavariable-pattern:
Expand Down
74 changes: 71 additions & 3 deletions docs/CI-POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ and `--max-findings`.

| Exit | Meaning |
|---:|---|
| `0` | Scan completed with `complete` aggregate coverage and the configured policy passed. In report-only mode, findings are allowed. |
| `1` | Coverage was complete and at least one canonical finding met or exceeded `--fail-on-severity`. |
| `2` | Invalid input, runtime failure, or aggregate coverage was `partial`/`unknown`. Coverage takes precedence over findings. |
| `0` | Scan completed with `complete` aggregate coverage, or `partial` coverage only through gaps named in `--accept-gaps`, and the configured policy passed. In report-only mode, findings are allowed. |
| `1` | Coverage was complete (or partial only through gaps named in `--accept-gaps`) and at least one canonical finding met or exceeded `--fail-on-severity`. |
| `2` | Invalid input, runtime failure, or aggregate coverage was `partial`/`unknown` (unless every cause is a gap named in `--accept-gaps`). Coverage takes precedence over findings. |
| `130` | Interrupted by `SIGINT`. |
| `143` | Terminated by `SIGTERM`. |

Expand All @@ -30,10 +30,78 @@ therefore reports aggregate `partial` and exits 2; it is useful as evidence, but
repository-wide CI result. Missing engines, failed components, truncation, bounded/skipped inputs,
secret uncertainty and missing execution evidence likewise cannot pass as clean.

## Declared coverage gaps

Some inputs CodeInspectus can see are analyzed by no bundled engine. Each such gap has a stable id,
makes aggregate coverage `partial`, and therefore exits 2 by default:

| Gap id | Meaning |
|---|---|
| `php-sast`, `ruby-sast`, `java-sast`, `kotlin-sast`, `go-sast`, `csharp-sast`, `rust-sast`, `swift-sast`, `c-sast`, `cpp-sast`, `scala-sast`, `dart-sast`, `objc-sast`, `elixir-sast`, `erlang-sast`, `lua-sast`, `perl-sast`, `groovy-sast`, `clojure-sast`, `haskell-sast`, `ocaml-sast`, `r-sast`, `julia-sast`, `zig-sast`, `solidity-sast`, `vb-sast` | Source files of a language that has no bundled code-pattern (Opengrep) rules. Native AI-check packs do not count as SAST. Server templates count toward their language: `.jsp`/`.jspx` (Java), `.erb` (Ruby), `.cshtml`/`.razor` (C#), `.blade.php` and `.twig` (PHP). `.m` is counted as Objective-C (`objc-sast`); MATLAB files share the extension and cannot be told apart. |
| `vue-sast`, `svelte-sast`, `astro-sast` | `.vue`, `.svelte` and `.astro` component files. Opengrep does not scan them and the native JavaScript packs do not read them, so their script code gets no SAST. |
| `sast-parse-errors` | Files Opengrep's parsers could only partly read (syntax, lexical or partial-parsing errors, for example syntax newer than its parser), so its rules did not see that code. Reported by the Opengrep run. A file with a timeout, memory limit or internal matching error is not this gap: it stays a coverage warning no gap id accepts. Residual risk: a repository can add a construct Opengrep cannot parse so the rest of a file goes unmatched; that is why it is a gap and never silent. Review the listed example files before accepting it. |
| `native-parse-errors` | Files the native packs' bounded JavaScript/TypeScript or Python lexers could not read, so the native AI and SAST checks skipped them. Includes files where the lexer cannot tell a regex from division. When the SAST scanner runs, Opengrep still scans them for its generic rules; the native-only checks (admin routes, Supabase Edge, React Native) have no Opengrep equivalent. Residual risk: a repository can add such a construct to keep a file from the native checks; review the examples before accepting. Separately from this gap, the native lexer is lightweight and can be evaded by deliberately crafted code while a file reads as parsed (see [Native pack limits](#native-pack-limits)). |
| `oversized-files` | Files over a native per-file size or call-count bound, or over the native JavaScript/TypeScript lexing caps (2 MiB characters per file, 64 KiB per line), skipped by the native checks. Typically vendored or minified bundles. Residual risk: an application file grown past a bound, or given one very long line, is skipped too; the examples show which. |
| `native-token-budget` | Files the native JavaScript/TypeScript packs did not parse because the 1,000,000-token project budget was spent; discovery continues in path order and counts every later source file. Residual risk: a repository can pad itself (large vendored or generated files early in path order) to push application files past the budget; when the SAST scanner runs, Opengrep still scans them for its generic rules, but the native-only checks have no Opengrep equivalent. Review the count and examples before accepting. |
| `express-routes-unresolved` | Files with Express admin-route candidates whose middleware or handler the check could not prove either way, so no finding and no clearance is reported for them. |
| `nextjs-routes-unresolved` | Files with Next.js admin API route handlers (`app/api/admin/**/route.*`, `pages/api/admin/**`) whose custom wrapper, custom guard or privileged operation the check could not prove either way (for example a project helper such as `parseRequest`), so no finding and no clearance is reported for them. Residual risk: a route without authorization that only calls such helpers is not reported; review the example files before accepting. |
| `symlinked-paths` | Symbolic links whose real target is inside the repository and is analysed under its own path by the walker that skipped the link (the native JavaScript walker or technology detection); path-sensitive checks (for example admin-route paths or manifest names) do not see it under the link's path. A link whose target is outside the repository, under a folder that walker ignores (such as `node_modules/`) or beyond its depth bound is not this gap: it stays a partial-coverage warning no id accepts. |
| `wordpress-plugins` | WordPress plugins and themes: `wpackagist-plugin/*` and `wpackagist-theme/*` entries in a `composer.lock`, and `wp-content/plugins/*` folders or files with a `Plugin Name:` header. No bundled engine has advisory data for them, so their versions are not checked. |
| `actions-unchecked` | Third-party `uses:` references in `.github/workflows/*.yml` (local `./` actions excluded). No bundled engine checks action versions against advisories; the GitHub Actions native rules still run. |
| `dev-deps-unscanned` | Maven dependencies in a `pom.xml` that Trivy 0.76.1 drops even with `--include-dev-deps`: `test`, `provided` or `system` scope, `<optional>true</optional>`, or a scope or optional flag set through a property (`${…}`), one entry per dependency; any `pom.xml` that cannot be read counts too. Their versions are not checked. Residual risk: test-scoped and optional dependencies still run in CI, on developer machines or in consumers' builds. |
| `deno-lock` | A `deno.lock` file; no engine reads it. |
| `bun-lockb` | A binary `bun.lockb` with no text `bun.lock` beside it. |
| `no-lockfile-npm` | A `package.json` that declares dependencies with no npm, Yarn, pnpm or Bun lockfile beside it. A lockfile in a parent folder covers it only when that parent declares its folder as a workspace (`workspaces` in the parent's `package.json`, as npm, Yarn and Bun use, or `packages` in `pnpm-workspace.yaml`); nested `examples/`, `docs/` or `website/` projects that are not workspaces are gaps. |
| `npm-shrinkwrap-unread` | An `npm-shrinkwrap.json` with no `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` or `bun.lock` beside it. Trivy 0.76.1 does not read `npm-shrinkwrap.json`. |
| `no-lockfile-composer` | A `composer.json` that requires packages with no `composer.lock` beside it. |

Files under the managed ignore list (`node_modules/`, `vendor/`, `build/`, `dist/`, `test/`, `tests/`,
the build-output folders file routing drops, such as `.next/`, `out/` and `.nuxt/`, and similar) are
not counted. Each gap reports its file count and up to five example paths.

Shell scripts, PowerShell scripts and Makefiles are not gaps: almost every repository has some, and
they are build or tooling glue. When a SAST scan finds them, `coverage_notes` (and the text summary
and the export, as informational evidence `scope:scripts`) say how many no bundled rule reads.

`--accept-gaps <id[,id]>` (MCP: `accept_gaps`) is an explicit opt-in for known gaps. Coverage still
reads `partial` and every gap stays listed (accepted ones are marked); CI passes coverage only when
every cause of non-complete coverage is an accepted gap id. Any other cause, including a gap you did
not name, still exits 2. An unknown id is an input error. Gaps travel in the JSON export as coverage
evidence with component `gap:<id>`, and each accepted id as an informational evidence item with
component `accepted-gap:<id>`, so export schema 3.0.0 is unchanged.

Gap ids are never removed. When a release closes a gap (for example, by adding PHP rules), its id is
no longer reported but stays valid: `--accept-gaps` and `accept_gaps` accept it as a no-op, and stored
scans that list it still load, so CI configurations keep working.

With `--baseline`, the accepted ids apply to both scans: a baseline or current scan that is partial
only through accepted gaps is a sound foundation, so findings can be proven New or Existing and the
comparison reads `complete`. Its `notes` name the gaps; a finding in input the baseline did not
analyze (for example a file it could not parse) reads New.

`scans compare` (SDK `compareHistory`) judges each stored scan by the gaps that scan accepted, but only
by language and component gaps (`<language>-sast`, `vue-sast`, `svelte-sast`, `astro-sast`): no engine
reports SAST findings in those inputs, so when both scans are complete or partial only through such
accepted gaps, absence can prove Resolved and New, and the comparison notes name the gaps. Accepted
`sast-parse-errors` and lockfile gaps (`no-lockfile-npm`, `no-lockfile-composer`,
`npm-shrinkwrap-unread`, `bun-lockb`, `deno-lock`) still make the comparison partial: an engine may
have analyzed that input in the other scan, so a missing finding there proves nothing. A scan that did
not accept its gaps also keeps the comparison partial.

JSON records `scan.configuration.policy_mode` and optional `fail_on_severity`. SARIF records the
same policy fields plus `aggregate_coverage` and `coverage_evidence` in run properties. SARIF is
written before the policy exit is returned, including exits 1 and 2, so CI can retain evidence.

### Native pack limits

The native packs (JavaScript/TypeScript, React Native, Express and Next.js admin routes, Supabase Edge
and the other native AI checks) use a lightweight lexer, not a full parser. They are pattern checks
for ordinary, non-adversarial code. Code written to evade them can hide a call from them, or show them
a fake guard, while the file still reads as parsed, so no gap is reported. When the SAST scanner runs,
Opengrep's full parser still scans the same files for its generic sinks, but the native-only checks
have no Opengrep equivalent.
Accepting a gap never makes this stronger. A full parser replacement is planned for 4.0.

## GitHub Actions reference

The shipped [copyable workflow](examples/codeinspectus-security.yml) separates the explicit,
Expand Down
3 changes: 3 additions & 0 deletions docs/CLI-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Shared scan options:
- `--output FILE` or `--output-dir DIRECTORY`
- `--allow-output-in-target` for an explicitly approved output directory inside the target
- `--no-compliance`
- `--accept-gaps <id[,id]>` (scan only): accept named coverage gaps such as `php-sast` or
`deno-lock` so they alone do not fail CI; coverage still reads `partial` and the gaps stay listed.
Gap ids are documented in [CI-POLICY.md](CI-POLICY.md#declared-coverage-gaps).

Policy and Git scope options:

Expand Down
7 changes: 7 additions & 0 deletions docs/RULE-PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,13 @@ Apache-2.0.** Convergent functional idioms; the public registry was referenced d
**no expression copied** (merger / scenes a faire). See LICENSE-PROVENANCE.md +
`docs/legal/RULE-DERIVATION-REVIEWS.md`.

Detection database 1.19.1 (2026-10-31) removed four pattern arms that could never report a finding
(`$T = Math.random()` in `crypto.yaml`, the template-literal `$FS.readFile` arm in `injection.yaml`,
and the template-literal `fetch`/`axios.get` arms in `ssrf.yaml`): each matched without binding the
metavariable the rule's `metavariable-pattern` then checks, so Opengrep reported an internal matching
error instead. Nothing was added; rule ids, languages, CWEs and messages are unchanged. Every rule
has true-positive cases in `fixtures/coverage-honesty-corpus/opengrep-rules/` (eval E75).

| Rule id | File | Langs | CWE | What it flags |
|---|---|---|---|---|
| `ci-baseline-weak-hash` | `crypto.yaml` | js,ts | CWE-327 | Weak hashing algorithm (MD5/SHA1) |
Expand Down
11 changes: 8 additions & 3 deletions docs/SCAN-HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ Each finding has one state:

- `Persisting`: the finding exists in both scans by fingerprint or stable deduplication identity.
- `Resolved`: the finding is absent from the new scan, both scans have complete canonical
whole-product coverage, every producer ran, and producer-component signatures are compatible.
whole-product coverage (or are partial only through language or component gaps that scan accepted,
such as `php-sast` or `vue-sast`, where no engine reports SAST findings), every producer ran, and
producer-component signatures are compatible. Accepted `sast-parse-errors` and lockfile gaps never
count: an engine may have analyzed that input in the other scan.
- `New`: the finding is absent from the old scan under compatible like-for-like producer coverage,
and does not occur in the complete bounded earlier history.
- `Reopened`: a three-point history proves that the same finding occurred before the old scan, was
Expand All @@ -71,5 +74,7 @@ missing V2 provenance is treated as uncertainty rather than invented evidence.

Malformed and traversal-shaped IDs are rejected before file access. The loader accepts only regular
managed files whose filename and embedded scan ID agree; symlinks, foreign records, and invalid JSON
are rejected or isolated. History commands return `0` only for a complete requested result and `2`
for usage errors, unavailable/corrupt/bounded history, or comparisons containing unknown states.
are rejected or isolated. History commands return `0` only for a complete requested result (a
comparison counts as complete when both scans are complete or partial only through accepted language
or component gaps) and `2` for usage errors, unavailable/corrupt/bounded history, or comparisons
containing unknown states.
Loading
Loading