mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
Fix fn and bump frontend packages (#57)
* chore(deps): update frontend dependencies to latest versions * fix: update reconnectTimer type and adjust tsconfig paths for consistency * fix: add toast to dependencies in FindingsPage component * fix: add toast to dependencies in FindingsPage component * fix: update language maturity metrics and improve Go validation handling * fix: update CHANGELOG with recent enhancements and dependency bumps * fix: format reconnectTimer initialization for improved readability
This commit is contained in:
parent
281699faae
commit
832533a8cd
15 changed files with 1210 additions and 1334 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -33,6 +33,12 @@ The biggest release since launch. The taint engine was rebuilt on top of an SSA
|
|||
- Cross-file SCC fixed-point. Mutually recursive functions across files now reach a joint convergence.
|
||||
- Demand-driven backwards analysis (off by default) annotates findings with cutoff diagnostics.
|
||||
- Direction-aware engine notes (`UnderReport`, `OverReport`, `Bail`) flow into confidence scoring, ranking, and the new `--require-converged` strict mode.
|
||||
- Synthetic field-write inheritance: `u.Path = "/foo"` no longer drops taint carried by other fields of `u`. Fixes Owncast CVE-2023-3188 (SSRF).
|
||||
- Phantom-Param-aware field suppression skips method/function references that share a base name with a tainted variable.
|
||||
- Validation err-check narrowing for the two-statement Go idiom `_, err := strconv.Atoi(input); if err != nil { return }` — `input` is marked validated on the surviving `err == nil` branch.
|
||||
- Go: `strings.Replace` / `strings.ReplaceAll` recognised as a sanitizer when the OLD literal contains a known-dangerous payload (shell metachars, path-traversal, HTML, SQL) and the NEW literal does not reintroduce one.
|
||||
- Go: literal-strip cap detection extended to shell metachars (`;`, `|`, `&`, `$`, backtick) and SQL metachars (`'`, `"`, `--`).
|
||||
- Go: `interpreted_string_literal` / `raw_string_literal` handled in tree-sitter so const-string arg extraction works for Go's double-quoted and backtick forms.
|
||||
|
||||
### Symbolic Execution
|
||||
|
||||
|
|
@ -54,6 +60,12 @@ The biggest release since launch. The taint engine was rebuilt on top of an SSA
|
|||
- C/C++ taint depth: output-parameter source propagation, implicit definitions for uninitialized declarations.
|
||||
- Negative test corpus (30 fixtures) and a 262-case benchmark with CI gates on rule-level Precision/Recall/F1.
|
||||
|
||||
### Detection metrics
|
||||
|
||||
- Aggregate rule-level F1 reaches **0.998** (P=0.995, R=1.000). All real-CVE fixtures fire; only one open FP (`go-safe-009`).
|
||||
- Go: 98.0% F1 on the 53-case corpus (1 FP / 0 FNs).
|
||||
- CVE-2023-3188 (owncast SSRF) now detects.
|
||||
|
||||
### CLI & Output
|
||||
|
||||
- `nyx serve`: local web UI on `localhost` only (refuses non-loopback binds).
|
||||
|
|
@ -89,6 +101,10 @@ The biggest release since launch. The taint engine was rebuilt on top of an SSA
|
|||
- Triage UI with database-backed decisions (true positive, false positive, deferred, suppressed) and `.nyx/triage.json` round-trip.
|
||||
- Scan history, rules management, and finding detail panels with evidence and flow visualization.
|
||||
- Vitest browser-side test suite wired into CI.
|
||||
- Bumped to React 19, Vite 8, TypeScript 6.0, ESLint 10, `@vitejs/plugin-react` 6, with aligned `@types/react*`.
|
||||
- `SSEContext`: typed `reconnectTimer` ref as `ReturnType<typeof setTimeout> | undefined` to satisfy TS 6's stricter `useRef` overloads.
|
||||
- `FindingsPage`: included `toast` in `useCallback` deps to avoid stale-closure warnings.
|
||||
- `tsconfig.json`: dropped `baseUrl`, using a relative `./src/*` path mapping instead.
|
||||
|
||||
### Removed
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ All 10 languages parse via tree-sitter and run through the full pipeline, but ru
|
|||
| **Beta** | Java, PHP, Ruby, Rust, Go | 94.1% to 100% | Yes, with light FP triage |
|
||||
| **Preview** | C, C++ | 100% on synthetic corpus | No. STL container flow, builder chains, and inline class member functions are tracked, but deep pointer aliasing and function pointers are not. Pair with clang-tidy or Clang Static Analyzer |
|
||||
|
||||
Aggregate rule-level F1: 99.3% (P=0.991, R=0.995). The single open FN is `cve-go-2023-3188-vulnerable` (owncast SSRF); the two open FPs (`go-safe-007`, `go-safe-009`) also sit on the Go side. Per-dimension detail and known blind spots live on the [Language maturity page](https://elicpeter.github.io/nyx/language-maturity.html).
|
||||
Aggregate rule-level F1: 99.8% (P=0.995, R=1.000). All real-CVE fixtures fire; the single open FP is `go-safe-009`. Per-dimension detail and known blind spots live on the [Language maturity page](https://elicpeter.github.io/nyx/language-maturity.html).
|
||||
|
||||
### Validated against real CVEs
|
||||
|
||||
|
|
@ -138,6 +138,7 @@ The corpus also holds a small set of vulnerable/patched pairs extracted from pub
|
|||
| [CVE-2023-26159](https://nvd.nist.gov/vuln/detail/CVE-2023-26159) | follow-redirects | TypeScript | SSRF |
|
||||
| [CVE-2022-30323](https://nvd.nist.gov/vuln/detail/CVE-2022-30323) | hashicorp/go-getter | Go | Command injection |
|
||||
| [CVE-2024-31450](https://nvd.nist.gov/vuln/detail/CVE-2024-31450) | owncast | Go | Path traversal |
|
||||
| [CVE-2023-3188](https://nvd.nist.gov/vuln/detail/CVE-2023-3188) | owncast | Go | SSRF |
|
||||
| [CVE-2015-7501](https://nvd.nist.gov/vuln/detail/CVE-2015-7501) | Apache Commons Collections | Java | Deserialization |
|
||||
| [CVE-2017-12629](https://nvd.nist.gov/vuln/detail/CVE-2017-12629) | Apache Solr | Java | Command injection |
|
||||
| [CVE-2013-0156](https://nvd.nist.gov/vuln/detail/CVE-2013-0156) | Ruby on Rails | Ruby | Deserialization |
|
||||
|
|
@ -149,8 +150,6 @@ The corpus also holds a small set of vulnerable/patched pairs extracted from pub
|
|||
| [CVE-2019-13132](https://nvd.nist.gov/vuln/detail/CVE-2019-13132) | ZeroMQ libzmq | C++ | Memory safety |
|
||||
| [CVE-2022-1941](https://nvd.nist.gov/vuln/detail/CVE-2022-1941) | Protocol Buffers | C++ | Memory safety |
|
||||
|
||||
`cve-go-2023-3188-vulnerable` (owncast SSRF) ships in the corpus too but is currently a known FN; it will move into the table once the engine fires on it.
|
||||
|
||||
Fixtures live under [`tests/benchmark/cve_corpus/`](tests/benchmark/cve_corpus/) with upstream attribution headers.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -4336,7 +4336,7 @@ limitations under the License.
|
|||
<ul class="license-used-by">
|
||||
<li><a href=" https://github.com/zrzka/anes-rs ">anes 0.1.6</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">async-compression 0.4.41</a></li>
|
||||
<li><a href=" https://github.com/BLAKE3-team/BLAKE3 ">blake3 1.8.4</a></li>
|
||||
<li><a href=" https://github.com/BLAKE3-team/BLAKE3 ">blake3 1.8.5</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">compression-codecs 0.4.37</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">compression-core 0.4.31</a></li>
|
||||
<li><a href=" https://github.com/cesarb/constant_time_eq ">constant_time_eq 0.4.2</a></li>
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@ The classifications here are grounded in three concrete signals:
|
|||
limitations the corpus does not stress, documented release-by-release in
|
||||
[`RESULTS.md`](https://github.com/elicpeter/nyx/blob/master/tests/benchmark/RESULTS.md).
|
||||
|
||||
As of 2026-04-29 the synthetic corpus has effectively saturated: nine of ten
|
||||
languages report rule-level F1 = 100.0% and Go reports 94.1% (two FPs and
|
||||
one FN on a real-CVE SSRF case, `cve-go-2023-3188-vulnerable`). Aggregate
|
||||
rule-level P=0.991, R=0.995, F1=0.993. That means F1 alone no longer
|
||||
differentiates tiers, so the differentiators are **rule depth**,
|
||||
**gated-sink coverage**, and **structural idioms the corpus does not fully
|
||||
stress** (deep pointer aliasing in C/C++, framework-specific context). All
|
||||
parser integrations use tree-sitter and are stable; parsing is not a
|
||||
differentiator.
|
||||
As of 2026-04-29 the synthetic corpus has effectively saturated: every
|
||||
real-CVE fixture fires and rule-level recall is 100%. Nine of ten
|
||||
languages report rule-level F1 = 100.0%; Go reports 98.0% on the back of
|
||||
a single safe-fixture FP. Aggregate rule-level P=0.995, R=1.000, F1=0.998.
|
||||
That means F1 alone no longer differentiates tiers, so the differentiators
|
||||
are **rule depth**, **gated-sink coverage**, and **structural idioms the
|
||||
corpus does not fully stress** (deep pointer aliasing in C/C++,
|
||||
framework-specific context). All parser integrations use tree-sitter and
|
||||
are stable; parsing is not a differentiator.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ differentiator.
|
|||
| Tier | Languages | F1 | What to expect |
|
||||
|------|-----------|----|----------------|
|
||||
| **Stable** | Python, JavaScript, TypeScript | 100% | Deep rule sets, gated sinks (argument-role-aware), framework detection, extensive fixtures, and the bulk of advanced-analysis (SSA two-level solve, context-sensitivity, symbolic execution, abstract interpretation) coverage. Safe to depend on in CI gates. |
|
||||
| **Beta** | Go, Java, PHP, Ruby, Rust | 94.1% to 100% | Solid mid-depth rule sets with narrower cap coverage and **no gated sinks**. Cross-file flows work; some idioms (variable-typed method receivers, framework context, string interpolation, match-arm guards) are partially modeled. Usable in CI; review FP/FN lists before tightening gates. |
|
||||
| **Beta** | Go, Java, PHP, Ruby, Rust | 98.0% to 100% | Solid mid-depth rule sets with narrower cap coverage and **no gated sinks**. Cross-file flows work; some idioms (variable-typed method receivers, framework context, string interpolation, match-arm guards) are partially modeled. Usable in CI; review FP/FN lists before tightening gates. |
|
||||
| **Preview** | C, C++ | 100% on synthetic corpus | Recent work taught the engine to follow taint through `std::vector` / `std::string` / map containers (including `c_str()`), through fluent builder chains like `Socket::builder().host(h).connect()`, and through inline class member functions. Function pointers and deeper pointer aliasing through `*p` / `p->field` are still not tracked. Rule-level scores against a corpus of obvious unsafe-API uses look perfect, but that is not the same as a clean audit on a real codebase. Pair with clang-tidy, Clang Static Analyzer, or Infer. |
|
||||
|
||||
---
|
||||
|
|
@ -90,15 +90,13 @@ differentiator.
|
|||
|
||||
### Beta tier
|
||||
|
||||
#### Go: 92.3% P / 96.0% R / 94.1% F1 *(53-case corpus, 2 FPs, 1 FN)*
|
||||
#### Go: 96.2% P / 100.0% R / 98.0% F1 *(53-case corpus, 1 FP, 0 FNs)*
|
||||
|
||||
- **Rule depth**: 4 source families, 4 sanitizer families, 9 sink matchers
|
||||
covering HTML, URL, Shell, SQL, SSRF, Crypto, and File I/O.
|
||||
- **Framework context**: Gin, Echo source matchers.
|
||||
- **Open weak spots**: `cve-go-2023-3188-vulnerable` (owncast SSRF) goes
|
||||
undetected, and two safe Go fixtures (`go-safe-007`, `go-safe-009`) draw
|
||||
spurious SQLi and CMDi findings respectively. These are the only
|
||||
imperfect language scores in the current corpus.
|
||||
- **Open weak spots**: one safe Go fixture (`go-safe-009`) draws a spurious
|
||||
CMDi finding.
|
||||
- **Known gaps**: no gated sinks, no deserialization class. `fmt.Sprintf`
|
||||
is deliberately not a sink. Cap coverage is narrower than the Stable
|
||||
tier and argument-role-aware sink modeling is not yet implemented for Go,
|
||||
|
|
|
|||
2005
frontend/package-lock.json
generated
2005
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -21,30 +21,30 @@
|
|||
"@tanstack/react-query": "^5.100.6",
|
||||
"elkjs": "^0.11.1",
|
||||
"graphology": "^0.26.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-router-dom": "^7.14.2",
|
||||
"sigma": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"jsdom": "^29.1.0",
|
||||
"license-checker-rseidelsohn": "^4.4.2",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.1",
|
||||
"vite": "^6.4.2",
|
||||
"vitest": "^4.1.1"
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ export function SSEProvider({ children }: { children: ReactNode }) {
|
|||
const [scanProgress, setScanProgress] = useState<ScanProgress | null>(null);
|
||||
const [isScanRunning, setIsScanRunning] = useState(false);
|
||||
const esRef = useRef<EventSource | null>(null);
|
||||
const reconnectTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (esRef.current) {
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ export function FindingsPage() {
|
|||
},
|
||||
);
|
||||
},
|
||||
[addSuppression],
|
||||
[addSuppression, toast],
|
||||
);
|
||||
|
||||
// ── Sort handler ──
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@
|
|||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/queryclient.ts","./src/api/types.ts","./src/api/mutations/baseline.ts","./src/api/mutations/config.ts","./src/api/mutations/rules.ts","./src/api/mutations/scans.ts","./src/api/mutations/triage.ts","./src/api/queries/config.ts","./src/api/queries/debug.ts","./src/api/queries/explorer.ts","./src/api/queries/findings.ts","./src/api/queries/health.ts","./src/api/queries/overview.ts","./src/api/queries/rules.ts","./src/api/queries/scans.ts","./src/api/queries/triage.ts","./src/components/copymarkdownbutton.tsx","./src/components/charts/horizontalbarchart.tsx","./src/components/charts/linechart.tsx","./src/components/data-display/codeviewer.tsx","./src/components/data-display/filetree.tsx","./src/components/explorer/analysisworkspace.tsx","./src/components/icons/icons.tsx","./src/components/layout/applayout.tsx","./src/components/layout/headerbar.tsx","./src/components/layout/sidebar.tsx","./src/components/overview/overviewwidgets.tsx","./src/components/ui/commandpalette.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/emptystate.tsx","./src/components/ui/errorstate.tsx","./src/components/ui/loadingstate.tsx","./src/components/ui/modal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/shortcutshelp.tsx","./src/components/ui/statcard.tsx","./src/components/ui/toaster.tsx","./src/contexts/ssecontext.tsx","./src/contexts/themecontext.tsx","./src/contexts/toastcontext.tsx","./src/graph/styles.ts","./src/graph/types.ts","./src/graph/adapters/callgraph.ts","./src/graph/adapters/cfg.ts","./src/graph/components/callgraphcanvas.tsx","./src/graph/components/cfggraphcanvas.tsx","./src/graph/components/graphtoolbar.tsx","./src/graph/hooks/useelklayout.ts","./src/graph/layout/elk.ts","./src/graph/layout/text.ts","./src/graph/reduction/cfgcompaction.ts","./src/graph/reduction/neighborhood.ts","./src/graph/rendering/sigma/sigmagraph.tsx","./src/graph/rendering/sigma/buildgraph.ts","./src/graph/rendering/sigma/edgeoverlay.ts","./src/hooks/usechordnavigation.ts","./src/hooks/usedebounce.ts","./src/hooks/usefiletree.ts","./src/hooks/usefindingsurlstate.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/usepagetitle.ts","./src/hooks/usepersistedstate.ts","./src/modals/codeviewermodal.tsx","./src/modals/newscanmodal.tsx","./src/pages/configpage.tsx","./src/pages/explorerpage.tsx","./src/pages/findingdetailpage.tsx","./src/pages/findingspage.tsx","./src/pages/overviewpage.tsx","./src/pages/rulespage.tsx","./src/pages/scancomparepage.tsx","./src/pages/scandetailpage.tsx","./src/pages/scanspage.tsx","./src/pages/triagepage.tsx","./src/pages/debug/abstractinterppage.tsx","./src/pages/debug/authanalysispage.tsx","./src/pages/debug/callgraphpage.tsx","./src/pages/debug/cfgviewerpage.tsx","./src/pages/debug/debuglayout.tsx","./src/pages/debug/functionselector.tsx","./src/pages/debug/pointerviewerpage.tsx","./src/pages/debug/ssaviewerpage.tsx","./src/pages/debug/summaryexplorerpage.tsx","./src/pages/debug/symexpage.tsx","./src/pages/debug/taintviewerpage.tsx","./src/pages/debug/typefactspage.tsx","./src/test/setup.ts","./src/test/api/client.test.ts","./src/test/components/pagination.test.tsx","./src/test/components/statcard.test.tsx","./src/test/components/statecomponents.test.tsx","./src/test/graph/cfgadapter.test.ts","./src/test/graph/compactgraph.test.ts","./src/test/graph/nodestyles.test.ts","./src/test/hooks/usedebounce.test.ts","./src/test/utils/findingmarkdown.test.ts","./src/test/utils/formatdate.test.ts","./src/test/utils/syntaxhighlight.test.ts","./src/test/utils/truncpath.test.ts","./src/utils/findingmarkdown.ts","./src/utils/formatdate.ts","./src/utils/parsenote.ts","./src/utils/syntaxhighlight.ts","./src/utils/truncpath.ts"],"version":"5.6.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/queryclient.ts","./src/api/types.ts","./src/api/mutations/baseline.ts","./src/api/mutations/config.ts","./src/api/mutations/rules.ts","./src/api/mutations/scans.ts","./src/api/mutations/triage.ts","./src/api/queries/config.ts","./src/api/queries/debug.ts","./src/api/queries/explorer.ts","./src/api/queries/findings.ts","./src/api/queries/health.ts","./src/api/queries/overview.ts","./src/api/queries/rules.ts","./src/api/queries/scans.ts","./src/api/queries/triage.ts","./src/components/copymarkdownbutton.tsx","./src/components/charts/horizontalbarchart.tsx","./src/components/charts/linechart.tsx","./src/components/data-display/codeviewer.tsx","./src/components/data-display/filetree.tsx","./src/components/explorer/analysisworkspace.tsx","./src/components/icons/icons.tsx","./src/components/layout/applayout.tsx","./src/components/layout/headerbar.tsx","./src/components/layout/sidebar.tsx","./src/components/overview/overviewwidgets.tsx","./src/components/ui/commandpalette.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/emptystate.tsx","./src/components/ui/errorstate.tsx","./src/components/ui/loadingstate.tsx","./src/components/ui/modal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/shortcutshelp.tsx","./src/components/ui/statcard.tsx","./src/components/ui/toaster.tsx","./src/contexts/ssecontext.tsx","./src/contexts/themecontext.tsx","./src/contexts/toastcontext.tsx","./src/graph/styles.ts","./src/graph/types.ts","./src/graph/adapters/callgraph.ts","./src/graph/adapters/cfg.ts","./src/graph/components/callgraphcanvas.tsx","./src/graph/components/cfggraphcanvas.tsx","./src/graph/components/graphtoolbar.tsx","./src/graph/hooks/useelklayout.ts","./src/graph/layout/elk.ts","./src/graph/layout/text.ts","./src/graph/reduction/cfgcompaction.ts","./src/graph/reduction/neighborhood.ts","./src/graph/rendering/sigma/sigmagraph.tsx","./src/graph/rendering/sigma/buildgraph.ts","./src/graph/rendering/sigma/edgeoverlay.ts","./src/hooks/usechordnavigation.ts","./src/hooks/usedebounce.ts","./src/hooks/usefiletree.ts","./src/hooks/usefindingsurlstate.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/usepagetitle.ts","./src/hooks/usepersistedstate.ts","./src/modals/codeviewermodal.tsx","./src/modals/newscanmodal.tsx","./src/pages/configpage.tsx","./src/pages/explorerpage.tsx","./src/pages/findingdetailpage.tsx","./src/pages/findingspage.tsx","./src/pages/overviewpage.tsx","./src/pages/rulespage.tsx","./src/pages/scancomparepage.tsx","./src/pages/scandetailpage.tsx","./src/pages/scanspage.tsx","./src/pages/triagepage.tsx","./src/pages/debug/abstractinterppage.tsx","./src/pages/debug/authanalysispage.tsx","./src/pages/debug/callgraphpage.tsx","./src/pages/debug/cfgviewerpage.tsx","./src/pages/debug/debuglayout.tsx","./src/pages/debug/functionselector.tsx","./src/pages/debug/pointerviewerpage.tsx","./src/pages/debug/ssaviewerpage.tsx","./src/pages/debug/summaryexplorerpage.tsx","./src/pages/debug/symexpage.tsx","./src/pages/debug/taintviewerpage.tsx","./src/pages/debug/typefactspage.tsx","./src/test/setup.ts","./src/test/api/client.test.ts","./src/test/components/pagination.test.tsx","./src/test/components/statcard.test.tsx","./src/test/components/statecomponents.test.tsx","./src/test/graph/cfgadapter.test.ts","./src/test/graph/compactgraph.test.ts","./src/test/graph/nodestyles.test.ts","./src/test/hooks/usedebounce.test.ts","./src/test/utils/findingmarkdown.test.ts","./src/test/utils/formatdate.test.ts","./src/test/utils/syntaxhighlight.test.ts","./src/test/utils/truncpath.test.ts","./src/utils/findingmarkdown.ts","./src/utils/formatdate.ts","./src/utils/parsenote.ts","./src/utils/syntaxhighlight.ts","./src/utils/truncpath.ts"],"version":"6.0.3"}
|
||||
|
|
@ -146,7 +146,10 @@ pub(super) fn extract_const_string_arg(
|
|||
let mut cursor = args.walk();
|
||||
let arg = args.named_children(&mut cursor).nth(index)?;
|
||||
match arg.kind() {
|
||||
"string" | "string_literal" => {
|
||||
// `string` / `string_literal` cover JS/TS, Python, Java, PHP, C/C++, Ruby, Rust;
|
||||
// `interpreted_string_literal` / `raw_string_literal` cover Go's
|
||||
// tree-sitter grammar (double-quoted vs. backtick-quoted forms).
|
||||
"string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal" => {
|
||||
let raw = text_of(arg, code)?;
|
||||
if raw.len() >= 2 {
|
||||
Some(raw[1..raw.len() - 1].to_string())
|
||||
|
|
@ -906,8 +909,10 @@ pub(super) fn extract_kwargs(call_node: Node, code: &[u8]) -> Vec<(String, Vec<S
|
|||
/// Policy is deliberately narrow and conservative: only literals that contain
|
||||
/// *known-dangerous* payloads earn a strip credit, so an arbitrary
|
||||
/// `.replace("foo", "bar")` is never promoted to a sanitizer.
|
||||
/// * `..`, `/`, `\\` → path-traversal → `Cap::FILE_IO`
|
||||
/// * `<`, `>` → HTML metachars → `Cap::HTML_ESCAPE`
|
||||
/// * `..`, `/`, `\\` → path-traversal → `Cap::FILE_IO`
|
||||
/// * `<`, `>` → HTML metachars → `Cap::HTML_ESCAPE`
|
||||
/// * `;`, `|`, `&`, `$`, `\`` → shell metachars → `Cap::SHELL_ESCAPE`
|
||||
/// * `'`, `"`, `--` → SQL metachars → `Cap::SQL_QUERY`
|
||||
pub(super) fn caps_stripped_by_literal_pattern(search: &str) -> Cap {
|
||||
let mut caps = Cap::empty();
|
||||
if search.contains("..") || search.contains('/') || search.contains('\\') {
|
||||
|
|
@ -916,6 +921,17 @@ pub(super) fn caps_stripped_by_literal_pattern(search: &str) -> Cap {
|
|||
if search.contains('<') || search.contains('>') {
|
||||
caps |= Cap::HTML_ESCAPE;
|
||||
}
|
||||
if search.contains(';')
|
||||
|| search.contains('|')
|
||||
|| search.contains('&')
|
||||
|| search.contains('$')
|
||||
|| search.contains('`')
|
||||
{
|
||||
caps |= Cap::SHELL_ESCAPE;
|
||||
}
|
||||
if search.contains('\'') || search.contains('"') || search.contains("--") {
|
||||
caps |= Cap::SQL_QUERY;
|
||||
}
|
||||
caps
|
||||
}
|
||||
|
||||
|
|
@ -1032,6 +1048,51 @@ pub(super) fn detect_rust_replace_chain_sanitizer(call_ast: Node, code: &[u8]) -
|
|||
None
|
||||
}
|
||||
|
||||
/// Recognise a Go `strings.Replace(s, OLD, NEW, n)` /
|
||||
/// `strings.ReplaceAll(s, OLD, NEW)` call that provably strips one of the
|
||||
/// known-dangerous metacharacter classes from its first argument.
|
||||
///
|
||||
/// Returns the union of caps stripped, or `None` when the pattern doesn't
|
||||
/// apply (so the caller falls back to normal unresolved-call propagation).
|
||||
///
|
||||
/// Mirrors [`detect_rust_replace_chain_sanitizer`] but for the single-call
|
||||
/// (non-method-chain) Go shape. The caller wires the resulting cap into
|
||||
/// the call's [`crate::labels::DataLabel::Sanitizer`] label, which the
|
||||
/// taint engine consumes via the standard sanitizer pathway — taint flows
|
||||
/// in on `s`, the matching cap is stripped from the result.
|
||||
pub(super) fn detect_go_replace_call_sanitizer(call_ast: Node, code: &[u8]) -> Option<Cap> {
|
||||
if call_ast.kind() != "call_expression" {
|
||||
return None;
|
||||
}
|
||||
// The call's `function` field is a `selector_expression` — `operand`
|
||||
// is the package ident (`strings`), `field` is the method ident.
|
||||
let func = call_ast.child_by_field_name("function")?;
|
||||
if func.kind() != "selector_expression" {
|
||||
return None;
|
||||
}
|
||||
let operand = func.child_by_field_name("operand")?;
|
||||
if text_of(operand, code).as_deref() != Some("strings") {
|
||||
return None;
|
||||
}
|
||||
let field = func.child_by_field_name("field")?;
|
||||
let method_name = text_of(field, code)?;
|
||||
if method_name != "Replace" && method_name != "ReplaceAll" {
|
||||
return None;
|
||||
}
|
||||
// Args layout: (s, old, new[, n]). Need positional args 1 (old) and
|
||||
// 2 (new) to be string literals.
|
||||
let old_lit = extract_const_string_arg(call_ast, 1, code)?;
|
||||
let new_lit = extract_const_string_arg(call_ast, 2, code)?;
|
||||
|
||||
// If the replacement itself reintroduces a dangerous sequence, don't
|
||||
// credit the strip — matches the Rust chain detector's policy.
|
||||
if !caps_stripped_by_literal_pattern(&new_lit).is_empty() {
|
||||
return None;
|
||||
}
|
||||
let caps = caps_stripped_by_literal_pattern(&old_lit);
|
||||
if caps.is_empty() { None } else { Some(caps) }
|
||||
}
|
||||
|
||||
/// Like `first_call_ident`, but also checks if `n` itself is a call node.
|
||||
/// `first_call_ident` only searches children, so when `n` IS the call
|
||||
/// expression (e.g. the argument `sanitize(cmd)`), this function catches it.
|
||||
|
|
|
|||
|
|
@ -49,12 +49,13 @@ use imports::{extract_import_bindings, extract_promisify_aliases};
|
|||
#[cfg(test)]
|
||||
use literals::has_sql_placeholders;
|
||||
use literals::{
|
||||
arg0_kind_and_interpolation, call_ident_of, def_use, detect_rust_replace_chain_sanitizer,
|
||||
extract_arg_callees, extract_arg_string_literals, extract_arg_uses, extract_const_keyword_arg,
|
||||
extract_const_string_arg, extract_destination_field_idents, extract_kwargs,
|
||||
extract_literal_rhs, find_call_node, find_call_node_deep, find_chained_inner_call,
|
||||
has_keyword_arg, has_only_literal_args, is_parameterized_query_call,
|
||||
java_chain_arg0_kind_for_method, ruby_chain_arg0_for_method, walk_chain_inner_call_args,
|
||||
arg0_kind_and_interpolation, call_ident_of, def_use, detect_go_replace_call_sanitizer,
|
||||
detect_rust_replace_chain_sanitizer, extract_arg_callees, extract_arg_string_literals,
|
||||
extract_arg_uses, extract_const_keyword_arg, extract_const_string_arg,
|
||||
extract_destination_field_idents, extract_kwargs, extract_literal_rhs, find_call_node,
|
||||
find_call_node_deep, find_chained_inner_call, has_keyword_arg, has_only_literal_args,
|
||||
is_parameterized_query_call, java_chain_arg0_kind_for_method, ruby_chain_arg0_for_method,
|
||||
walk_chain_inner_call_args,
|
||||
};
|
||||
use params::{
|
||||
compute_container_and_kind, extract_param_meta, inject_framework_param_sources,
|
||||
|
|
@ -1788,6 +1789,27 @@ pub(super) fn push_node<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
// Pattern-based sanitizer synthesis for Go's `strings.Replace` /
|
||||
// `strings.ReplaceAll`. When the call's OLD literal contains a known
|
||||
// dangerous payload (shell metachars, path-traversal, HTML, SQL) and
|
||||
// the NEW literal does not reintroduce one, treat the call as a
|
||||
// Sanitizer over the matching caps. Same precedence as the Rust
|
||||
// chain synthesis: explicit Sanitizer labels win, but otherwise the
|
||||
// synthesised label feeds the standard sanitizer pathway in the
|
||||
// taint engine. Motivated by helpers like
|
||||
// `func validate(s string) string { return strings.ReplaceAll(s, ";", "") }`
|
||||
// whose return is appended to a slice that later flows into
|
||||
// `exec.Command(slice[i])`.
|
||||
if lang == "go" && !labels.iter().any(|l| matches!(l, DataLabel::Sanitizer(_))) {
|
||||
if let Some(cn) = call_ast {
|
||||
if cn.kind() == "call_expression" {
|
||||
if let Some(caps) = detect_go_replace_call_sanitizer(cn, code) {
|
||||
labels.push(DataLabel::Sanitizer(caps));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shape-based sanitizer synthesis for Ruby ActiveRecord query methods.
|
||||
// The static label table marks `where` / `order` / `pluck` / `group` /
|
||||
// `having` / `joins` as `Sink(SQL_QUERY)` because their string-interpolation
|
||||
|
|
|
|||
|
|
@ -988,6 +988,30 @@ fn compute_succ_states(
|
|||
Some(transfer.interner),
|
||||
);
|
||||
|
||||
// Validation-call err-check narrowing. When the condition
|
||||
// is an `err`-check (e.g. `if err != nil`) and `err` is the
|
||||
// result of a known value-producing validator
|
||||
// (`strconv.Atoi`, `parseInt`, etc.), mark the validator's
|
||||
// input argument(s) as validated on the success branch
|
||||
// (where `err` is null / `Ok` / no exception). Mirrors the
|
||||
// ValidationCall pathway but for the two-statement
|
||||
// validation idiom common in Go:
|
||||
// `_, err := strconv.Atoi(input); if err != nil { return }`
|
||||
// post-condition: input is provably a numeric string on the
|
||||
// surviving (`err == nil`) branch, so downstream sinks like
|
||||
// `db.Query("... " + input)` should suppress.
|
||||
if matches!(kind, PredicateKind::ErrorCheck) {
|
||||
apply_validation_err_check_narrowing(
|
||||
&mut true_state,
|
||||
&mut false_state,
|
||||
cond_text,
|
||||
&cond_info.condition_vars,
|
||||
ssa,
|
||||
block.id,
|
||||
transfer.interner,
|
||||
);
|
||||
}
|
||||
|
||||
// Constraint refinement
|
||||
//
|
||||
// `lower_condition` returns a ConditionExpr that represents the
|
||||
|
|
@ -1184,6 +1208,152 @@ fn apply_branch_predicates(
|
|||
}
|
||||
}
|
||||
|
||||
/// Mark the input arguments of a value-producing validator as validated
|
||||
/// on the success branch of a downstream `err`-check.
|
||||
///
|
||||
/// Recognised idiom (most idiomatic in Go):
|
||||
///
|
||||
/// ```text
|
||||
/// _, err := strconv.Atoi(input)
|
||||
/// if err != nil { return }
|
||||
/// // → input is provably a valid integer string on the surviving branch
|
||||
/// ```
|
||||
///
|
||||
/// Walks `cond_info.condition_vars` to locate the SSA value bound to the
|
||||
/// condition's `err`/result variable, finds the SsaInst that defined that
|
||||
/// value, and — if the defining op is a [`SsaOp::Call`] to a
|
||||
/// [`crate::ssa::type_facts::is_int_producing_callee`] — copies the call's
|
||||
/// argument variable names into `validated_must` / `validated_may` on the
|
||||
/// `err == null` branch.
|
||||
///
|
||||
/// The "success" branch direction is determined from `cond_text`:
|
||||
///
|
||||
/// * `err == nil` / `err == None` / `error == nil` / `is_ok()` → TRUE branch
|
||||
/// * `err != nil` / `error != nil` / `is_err()` → FALSE branch
|
||||
///
|
||||
/// Strict-additive: when the condition does not match the err-check shape,
|
||||
/// the defining op is not a Call, the callee is not recognised as a
|
||||
/// validator, or the arg has no SSA-level var_name to mark, the function
|
||||
/// is a no-op.
|
||||
fn apply_validation_err_check_narrowing(
|
||||
true_state: &mut SsaTaintState,
|
||||
false_state: &mut SsaTaintState,
|
||||
cond_text: &str,
|
||||
condition_vars: &[String],
|
||||
ssa: &SsaBody,
|
||||
block: BlockId,
|
||||
interner: &SymbolInterner,
|
||||
) {
|
||||
if condition_vars.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Determine which branch corresponds to "err is null / Ok / no error".
|
||||
// Defaults to FALSE for `err != nil`-style; flips to TRUE for
|
||||
// `err == nil`-style and `is_ok()`.
|
||||
let lower = cond_text.to_ascii_lowercase();
|
||||
let success_branch_is_true = lower.contains("== nil")
|
||||
|| lower.contains("== none")
|
||||
|| lower.contains("is none")
|
||||
|| lower.contains("is_ok")
|
||||
|| lower.contains("=== null")
|
||||
|| lower.contains("== null");
|
||||
|
||||
// Resolve `err`'s reaching SSA value (last def in this or earlier block).
|
||||
// We restrict to single-var conditions to avoid mis-attributing
|
||||
// validation when the condition mixes err and another variable
|
||||
// (e.g. `err != nil || other`).
|
||||
if condition_vars.len() != 1 {
|
||||
return;
|
||||
}
|
||||
let err_name = condition_vars[0].as_str();
|
||||
let err_val = match resolve_var_to_ssa_value(err_name, ssa, block) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Find the defining SsaInst. Search across blocks because the
|
||||
// assignment might have happened in a predecessor.
|
||||
let def_inst = ssa
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| b.body.iter())
|
||||
.find(|i| i.value == err_val);
|
||||
let Some(def_inst) = def_inst else { return };
|
||||
|
||||
let SsaOp::Call {
|
||||
ref callee,
|
||||
ref args,
|
||||
..
|
||||
} = def_inst.op
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !crate::ssa::type_facts::is_int_producing_callee(callee) {
|
||||
return;
|
||||
}
|
||||
// Collect candidate input arg variable names: every SSA value across
|
||||
// every positional arg group, looked up by var_name. Conservative —
|
||||
// we mark *all* of them validated rather than guessing which arg the
|
||||
// validator narrows. The validators we recognise here
|
||||
// (`strconv.Atoi`, `parseInt`, `ParseFloat`, …) all take exactly one
|
||||
// primary string argument, so in practice this collects one name.
|
||||
let mut arg_names: SmallVec<[String; 2]> = SmallVec::new();
|
||||
for arg_group in args {
|
||||
for &v in arg_group {
|
||||
if let Some(name) = ssa
|
||||
.value_defs
|
||||
.get(v.0 as usize)
|
||||
.and_then(|vd| vd.var_name.as_deref())
|
||||
{
|
||||
if !arg_names.iter().any(|s: &String| s == name) {
|
||||
arg_names.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if arg_names.is_empty() {
|
||||
return;
|
||||
}
|
||||
let success_state = if success_branch_is_true {
|
||||
true_state
|
||||
} else {
|
||||
false_state
|
||||
};
|
||||
for name in &arg_names {
|
||||
if let Some(sym) = interner.get(name) {
|
||||
success_state.validated_may.insert(sym);
|
||||
success_state.validated_must.insert(sym);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the latest reaching SSA definition for `var_name` at the end of
|
||||
/// `block`. Mirrors `crate::constraint::lower::resolve_single_var` but
|
||||
/// avoids the cross-module privacy leak: callers in this module need it
|
||||
/// for branch narrowing on err-check shapes.
|
||||
fn resolve_var_to_ssa_value(var_name: &str, ssa: &SsaBody, block: BlockId) -> Option<SsaValue> {
|
||||
let mut best_in_block: Option<SsaValue> = None;
|
||||
let mut best_outside: Option<SsaValue> = None;
|
||||
for (idx, vd) in ssa.value_defs.iter().enumerate() {
|
||||
if vd.var_name.as_deref() != Some(var_name) {
|
||||
continue;
|
||||
}
|
||||
let v = SsaValue(idx as u32);
|
||||
if vd.block == block {
|
||||
best_in_block = Some(match best_in_block {
|
||||
Some(existing) if existing.0 > v.0 => existing,
|
||||
_ => v,
|
||||
});
|
||||
} else {
|
||||
best_outside = Some(match best_outside {
|
||||
Some(existing) if existing.0 > v.0 => existing,
|
||||
_ => v,
|
||||
});
|
||||
}
|
||||
}
|
||||
best_in_block.or(best_outside)
|
||||
}
|
||||
|
||||
/// Apply Rust path-rejection / path-assertion branch narrowing to the
|
||||
/// true/false branch states produced by `compute_succ_states`.
|
||||
///
|
||||
|
|
@ -3764,6 +3934,25 @@ pub(super) fn transfer_inst(
|
|||
}
|
||||
}
|
||||
|
||||
// Synthetic field-write inheritance. When SSA lowering emits
|
||||
// `u_new = Assign(rhs)` to model `u.f = rhs` (an obj-update
|
||||
// synth), `u_new` represents the same logical object after the
|
||||
// field write — it retains every other field's taint. The
|
||||
// base-only Assign uses include only the rhs, so without this
|
||||
// step a clean rhs (`u.Path = "/foo"`) would zero out every
|
||||
// tainted field on the prior `u`. Owncast CVE-2023-3188 hit
|
||||
// this: `requestURL.Path = "/.well-known/webfinger"` killed the
|
||||
// tainted host carried by `requestURL` from `url.Parse(tainted)`.
|
||||
if let Some((receiver, _fid)) = ssa.field_writes.get(&inst.value).copied() {
|
||||
if let Some(taint) = state.get(receiver) {
|
||||
combined_caps |= taint.caps;
|
||||
inherited_summary |= taint.uses_summary;
|
||||
for orig in &taint.origins {
|
||||
push_origin_bounded(&mut combined_origins, *orig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sanitizer
|
||||
combined_caps &= !sanitizer_bits;
|
||||
|
||||
|
|
@ -6254,7 +6443,7 @@ fn collect_tainted_sink_values(
|
|||
}
|
||||
}
|
||||
}
|
||||
apply_field_aware_suppression(&mut result, inst, state, sink_caps, ssa);
|
||||
apply_field_aware_suppression(&mut result, inst, info, state, sink_caps, ssa);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -6283,7 +6472,7 @@ fn collect_tainted_sink_values(
|
|||
}
|
||||
}
|
||||
}
|
||||
apply_field_aware_suppression(&mut result, inst, state, sink_caps, ssa);
|
||||
apply_field_aware_suppression(&mut result, inst, info, state, sink_caps, ssa);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -6298,7 +6487,7 @@ fn collect_tainted_sink_values(
|
|||
check_heap_taint(v, &mut result);
|
||||
}
|
||||
|
||||
apply_field_aware_suppression(&mut result, inst, state, sink_caps, ssa);
|
||||
apply_field_aware_suppression(&mut result, inst, info, state, sink_caps, ssa);
|
||||
result
|
||||
}
|
||||
|
||||
|
|
@ -6308,6 +6497,7 @@ fn collect_tainted_sink_values(
|
|||
fn apply_field_aware_suppression(
|
||||
result: &mut Vec<(SsaValue, Cap, SmallVec<[TaintOrigin; 2]>)>,
|
||||
inst: &SsaInst,
|
||||
info: &NodeInfo,
|
||||
state: &SsaTaintState,
|
||||
sink_caps: Cap,
|
||||
ssa: &SsaBody,
|
||||
|
|
@ -6334,16 +6524,48 @@ fn apply_field_aware_suppression(
|
|||
};
|
||||
// Collect all field values matching "base.X" (excluding method-call
|
||||
// expressions and the callee itself).
|
||||
//
|
||||
// Phantom Param ops with dotted var_names (e.g. `u.String` for the
|
||||
// method ref in `u.String()`) represent free-identifier references
|
||||
// hoisted by SSA lowering, not real data field accesses. Owncast
|
||||
// CVE-2023-3188 hit this: `http.DefaultClient.Get(u.String())`
|
||||
// includes both `u` (tainted) and `u.String` (untainted phantom)
|
||||
// as uses; treating `u.String` as a clean field of `u` suppressed
|
||||
// the SSRF. But JS object-field FP guards (e.g.
|
||||
// `db.query(obj.safeField)` with `obj.unsafeField` tainted) need
|
||||
// the opposite — `obj.safeField` is a real field access and SHOULD
|
||||
// count as a clean field. The CFG distinguishes the two via
|
||||
// `arg_callees`: when an argument expression is itself a call, its
|
||||
// callee text is recorded; pure member-access args leave the slot
|
||||
// `None`. Skip phantoms whose var_name appears as an arg_callee
|
||||
// (the Go case), keep phantoms representing field reads (the JS
|
||||
// case) so suppression still fires.
|
||||
let field_values: SmallVec<[SsaValue; 4]> = all_used
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&u| {
|
||||
u != *v
|
||||
&& ssa.def_of(u).var_name.as_deref().is_some_and(|uname| {
|
||||
uname.starts_with(&prefix)
|
||||
&& callee_name.map_or(true, |cn| uname != cn)
|
||||
&& !is_likely_method_expression(uname)
|
||||
})
|
||||
if u == *v {
|
||||
return false;
|
||||
}
|
||||
let uname = match ssa.def_of(u).var_name.as_deref() {
|
||||
Some(n) => n,
|
||||
None => return false,
|
||||
};
|
||||
if !uname.starts_with(&prefix) {
|
||||
return false;
|
||||
}
|
||||
if callee_name.is_some_and(|cn| uname == cn) {
|
||||
return false;
|
||||
}
|
||||
if is_likely_method_expression(uname) {
|
||||
return false;
|
||||
}
|
||||
if is_phantom_param_value(u, ssa)
|
||||
&& info.arg_callees.iter().any(|c| c.as_deref() == Some(uname))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
// Suppress base only if there ARE field values AND ALL of them
|
||||
|
|
@ -6357,6 +6579,21 @@ fn apply_field_aware_suppression(
|
|||
});
|
||||
}
|
||||
|
||||
/// Check whether an SSA value is defined by a phantom `Param` op (a free
|
||||
/// identifier like `u.String` hoisted by SSA lowering, not a real positional
|
||||
/// parameter). Used by field-aware suppression to skip method/function
|
||||
/// references that share a base name with a tainted variable.
|
||||
fn is_phantom_param_value(v: SsaValue, ssa: &SsaBody) -> bool {
|
||||
let def = ssa.def_of(v);
|
||||
let block = &ssa.blocks[def.block.0 as usize];
|
||||
block
|
||||
.phis
|
||||
.iter()
|
||||
.chain(block.body.iter())
|
||||
.find(|inst| inst.value == v)
|
||||
.is_some_and(|inst| matches!(inst.op, SsaOp::Param { .. } | SsaOp::SelfParam))
|
||||
}
|
||||
|
||||
/// Check if a dotted var_name looks like a method call expression rather than
|
||||
/// a field access. E.g., "items.join" where "join" is a method name, vs
|
||||
/// "obj.data" which is a field access.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Real disclosed CVEs reduced to minimal reproducers, vulnerable + patched pair pe
|
|||
| CVE-2025-64430 | JavaScript | Parse Server | Apache-2.0 | SSRF | detected |
|
||||
| CVE-2023-26159 | TypeScript | follow-redirects | MIT | SSRF | detected |
|
||||
| CVE-2022-30323 | Go | hashicorp/go-getter | MPL-2.0 | CMDI | detected |
|
||||
| CVE-2023-3188 | Go | owncast | MIT | SSRF | open FN |
|
||||
| CVE-2023-3188 | Go | owncast | MIT | SSRF | detected |
|
||||
| CVE-2024-31450 | Go | owncast | MIT | path_traversal | detected |
|
||||
| CVE-2015-7501 | Java | Apache Commons Collections | Apache-2.0 | Deserialization | detected |
|
||||
| CVE-2017-12629 | Java | Apache Solr | Apache-2.0 | CMDI | detected |
|
||||
|
|
@ -60,6 +60,7 @@ Most recent first. Metrics are rule-level on the corpus size at that point.
|
|||
|
||||
| Date | Change | Corpus | P | R | F1 |
|
||||
|------------|------------------------------------------------------------------------------|--------|-------|-------|-------|
|
||||
| 2026-04-29 | Phantom-Param-aware field suppression: CVE-2023-3188 detected, FP guards hold | 432 | 0.995 | 1.000 | 0.998 |
|
||||
| 2026-04-28 | Ruby bare `Kernel#open` CMDI sink, exact-match sigil on label matchers | 428 | 0.995 | 1.000 | 0.998 |
|
||||
| 2026-04-28 | Go SSRF/FILE_IO sink expansion (`http.DefaultClient.*`, `os.Remove`/`WriteFile`) plus Decode-writeback container op | 426 | 0.995 | 1.000 | 0.998 |
|
||||
| 2026-04-27 | JS chained-method inner-gate classification (`http.get(u, cb).on(...)`) | 422 | 0.994 | 1.000 | 0.997 |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"benchmark_version": "1.0",
|
||||
"timestamp": "2026-04-29T03:43:28Z",
|
||||
"timestamp": "2026-04-29T05:42:03Z",
|
||||
"scanner_version": "0.5.0",
|
||||
"scanner_config": {
|
||||
"analysis_mode": "Full",
|
||||
|
|
@ -1184,13 +1184,17 @@
|
|||
"language": "go",
|
||||
"vuln_class": "ssrf",
|
||||
"is_vulnerable": true,
|
||||
"outcome_file_level": "FN",
|
||||
"outcome_rule_level": "FN",
|
||||
"outcome_location_level": "FN",
|
||||
"matched_rule_ids": [],
|
||||
"outcome_file_level": "TP",
|
||||
"outcome_rule_level": "TP",
|
||||
"outcome_location_level": "TP",
|
||||
"matched_rule_ids": [
|
||||
"taint-unsanitised-flow (source 84:13)"
|
||||
],
|
||||
"unexpected_rule_ids": [],
|
||||
"all_finding_ids": [],
|
||||
"security_finding_count": 0,
|
||||
"all_finding_ids": [
|
||||
"taint-unsanitised-flow (source 84:13)"
|
||||
],
|
||||
"security_finding_count": 1,
|
||||
"non_security_finding_count": 0
|
||||
},
|
||||
{
|
||||
|
|
@ -2170,19 +2174,13 @@
|
|||
"language": "go",
|
||||
"vuln_class": "safe",
|
||||
"is_vulnerable": false,
|
||||
"outcome_file_level": "FP",
|
||||
"outcome_rule_level": "FP",
|
||||
"outcome_file_level": "TN",
|
||||
"outcome_rule_level": "TN",
|
||||
"outcome_location_level": null,
|
||||
"matched_rule_ids": [],
|
||||
"unexpected_rule_ids": [
|
||||
"go.sqli.query_concat",
|
||||
"taint-unsanitised-flow (source 10:11)"
|
||||
],
|
||||
"all_finding_ids": [
|
||||
"go.sqli.query_concat",
|
||||
"taint-unsanitised-flow (source 10:11)"
|
||||
],
|
||||
"security_finding_count": 2,
|
||||
"unexpected_rule_ids": [],
|
||||
"all_finding_ids": [],
|
||||
"security_finding_count": 0,
|
||||
"non_security_finding_count": 0
|
||||
},
|
||||
{
|
||||
|
|
@ -7773,22 +7771,22 @@
|
|||
}
|
||||
],
|
||||
"aggregate_file_level": {
|
||||
"tp": 215,
|
||||
"fp": 2,
|
||||
"fn_": 1,
|
||||
"tn": 214,
|
||||
"precision": 0.9907834101382489,
|
||||
"recall": 0.9953703703703703,
|
||||
"f1": 0.9930715935334872
|
||||
"tp": 216,
|
||||
"fp": 1,
|
||||
"fn_": 0,
|
||||
"tn": 215,
|
||||
"precision": 0.9953917050691244,
|
||||
"recall": 1.0,
|
||||
"f1": 0.997690531177829
|
||||
},
|
||||
"aggregate_rule_level": {
|
||||
"tp": 215,
|
||||
"fp": 2,
|
||||
"fn_": 1,
|
||||
"tn": 214,
|
||||
"precision": 0.9907834101382489,
|
||||
"recall": 0.9953703703703703,
|
||||
"f1": 0.9930715935334872
|
||||
"tp": 216,
|
||||
"fp": 1,
|
||||
"fn_": 0,
|
||||
"tn": 215,
|
||||
"precision": 0.9953917050691244,
|
||||
"recall": 1.0,
|
||||
"f1": 0.997690531177829
|
||||
},
|
||||
"by_language": {
|
||||
"c": {
|
||||
|
|
@ -7810,13 +7808,13 @@
|
|||
"f1": 1.0
|
||||
},
|
||||
"go": {
|
||||
"tp": 24,
|
||||
"fp": 2,
|
||||
"fn_": 1,
|
||||
"tn": 26,
|
||||
"precision": 0.9230769230769231,
|
||||
"recall": 0.96,
|
||||
"f1": 0.9411764705882353
|
||||
"tp": 25,
|
||||
"fp": 1,
|
||||
"fn_": 0,
|
||||
"tn": 27,
|
||||
"precision": 0.9615384615384616,
|
||||
"recall": 1.0,
|
||||
"f1": 0.9803921568627451
|
||||
},
|
||||
"java": {
|
||||
"tp": 17,
|
||||
|
|
@ -8002,9 +8000,9 @@
|
|||
},
|
||||
"safe": {
|
||||
"tp": 0,
|
||||
"fp": 2,
|
||||
"fp": 1,
|
||||
"fn_": 0,
|
||||
"tn": 214,
|
||||
"tn": 215,
|
||||
"precision": 0.0,
|
||||
"recall": 1.0,
|
||||
"f1": 0.0
|
||||
|
|
@ -8028,13 +8026,13 @@
|
|||
"f1": 1.0
|
||||
},
|
||||
"ssrf": {
|
||||
"tp": 25,
|
||||
"tp": 26,
|
||||
"fp": 0,
|
||||
"fn_": 1,
|
||||
"fn_": 0,
|
||||
"tn": 0,
|
||||
"precision": 1.0,
|
||||
"recall": 0.9615384615384616,
|
||||
"f1": 0.9803921568627451
|
||||
"recall": 1.0,
|
||||
"f1": 1.0
|
||||
},
|
||||
"xss": {
|
||||
"tp": 23,
|
||||
|
|
@ -8048,13 +8046,13 @@
|
|||
},
|
||||
"by_confidence": {
|
||||
">=High": {
|
||||
"tp": 89,
|
||||
"tp": 90,
|
||||
"fp": 90,
|
||||
"fn_": 127,
|
||||
"fn_": 126,
|
||||
"tn": 126,
|
||||
"precision": 0.4972067039106145,
|
||||
"recall": 0.41203703703703703,
|
||||
"f1": 0.4506329113924051
|
||||
"precision": 0.5,
|
||||
"recall": 0.4166666666666667,
|
||||
"f1": 0.45454545454545453
|
||||
},
|
||||
">=Low": {
|
||||
"tp": 94,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue