mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
fix failing ci + update docs
This commit is contained in:
parent
db35cdff2c
commit
061e1f981c
7 changed files with 201 additions and 76 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -14,6 +14,7 @@
|
|||
.DS_Store
|
||||
.z3-trace
|
||||
.pitboss
|
||||
.eval-corpus
|
||||
.node_modules-target
|
||||
node_modules
|
||||
__pycache__/
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -1,7 +1,7 @@
|
|||
<div align="center">
|
||||
<img src="assets/nyx-readme-header.png" alt="NYX" width="640"/>
|
||||
|
||||
**A local-first security scanner with a browser UI. Scan your repo and triage in your browser, with no cloud and no account.**
|
||||
**A local-first security scanner with sandboxed dynamic verification and a browser UI. Scan your repo and triage in your browser, with no cloud and no account.**
|
||||
|
||||
[](https://crates.io/crates/nyx-scanner)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
|
@ -18,7 +18,7 @@ English · [简体中文](./README.zh-CN.md)
|
|||
|
||||
## Scan locally, browse locally
|
||||
|
||||
Nyx runs a cross-language taint analysis on your repository, then serves the results to a React UI bound to `127.0.0.1`. You get a finding list with severity, evidence, and a step-by-step **flow visualiser** that walks the dataflow from source → sanitizer → sink. Triage decisions persist to `.nyx/triage.json`, which commits alongside your code so the team shares one triage state.
|
||||
Nyx runs cross-language taint analysis on your repository, then verifies Medium or higher confidence findings by running small sandboxed harnesses against the real code. Results are served to a React UI bound to `127.0.0.1`. You get severity, static evidence, dynamic verdicts, and a step-by-step **flow visualiser** that walks the dataflow from source → sanitizer → sink. Triage decisions persist to `.nyx/triage.json`, which commits alongside your code so the team shares one triage state.
|
||||
|
||||
```bash
|
||||
cargo install nyx-scanner
|
||||
|
|
@ -26,7 +26,7 @@ nyx scan # runs the analyzer, caches findings in .nyx/
|
|||
nyx serve # opens http://localhost:9700 in your browser
|
||||
```
|
||||
|
||||
Everything stays on your machine: loopback-only bind, host-header enforcement, CSRF on every mutation, no telemetry, no login.
|
||||
Everything stays on your machine: loopback-only bind, host-header enforcement, CSRF on every mutation, no remote telemetry, no login.
|
||||
|
||||
<p align="center"><img src="assets/screenshots/overview.png" alt="Overview dashboard for a small JS app: Health Score C 78 with the five-component breakdown (Severity pressure, Confidence quality, Trend, Triage coverage, Regression resistance), 3 findings detected, OWASP A03 and A02 buckets, confidence distribution and issue category bars, top affected files" width="900"/></p>
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ Everything stays on your machine: loopback-only bind, host-header enforcement, C
|
|||
|---|---|
|
||||
| **Overview** | Dashboard: finding counts by severity, top offenders, engine profile summary |
|
||||
| **Findings** | Browsable list with severity badges, triage status, rule filter, language filter |
|
||||
| **Finding detail** | Flow-path visualiser with numbered steps (source → sanitizer → sink), code snippets, evidence, cross-file markers, triage dropdown |
|
||||
| **Finding detail** | Flow-path visualiser with numbered steps (source → sanitizer → sink), dynamic verdicts, code snippets, evidence, cross-file markers, triage dropdown |
|
||||
| **Triage** | Bulk update states (open, investigating, fixed, false_positive, accepted_risk, suppressed), audit trail, import/export JSON |
|
||||
| **Explorer** | File tree with per-file symbol list and finding overlay |
|
||||
| **Scans** | Run history, metrics, diff two scans to see what changed |
|
||||
|
|
@ -190,13 +190,14 @@ flowchart LR
|
|||
Summaries --> Index["SQLite index<br/>optional incremental cache"]
|
||||
Index --> Pass2["Pass 2 cross-file<br/>global summaries, k=1 inline, SCC fixpoint"]
|
||||
Pass2 --> Rank["Rank and dedupe<br/>severity, evidence, exploitability"]
|
||||
Rank --> Output["Console, JSON, SARIF<br/>and browser UI"]
|
||||
Rank --> Verify["Dynamic verification<br/>sandboxed harnesses, verdicts"]
|
||||
Verify --> Output["Console, JSON, SARIF<br/>and browser UI"]
|
||||
```
|
||||
|
||||
1. **Pass 1**: parse each file via tree-sitter, build an intra-procedural CFG (petgraph), lower to pruned SSA (Cytron phi insertion over dominance frontiers), and export per-function summaries (source/sanitizer/sink caps, taint transforms, points-to, callees).
|
||||
2. **Summary merge**: union all per-file summaries into a `GlobalSummaries` map.
|
||||
3. **Pass 2**: re-analyze each file with cross-file context under bounded context sensitivity (k=1 inlining for intra-file callees, SCC fixpoint capped at 64 iterations, and summary fallback for callees above the inline body-size cap). A forward dataflow worklist propagates taint through the SSA lattice with guaranteed convergence. Call-graph SCCs iterate to fixed-point (within the cap) so mutually recursive functions get accurate summaries.
|
||||
4. **Rank, dedupe, emit**: findings are scored by severity × evidence strength × source-kind exploitability, then emitted to console, JSON, or SARIF.
|
||||
4. **Rank, dedupe, verify, emit**: findings are scored by severity × evidence strength × source-kind exploitability. Medium or higher confidence findings are dynamically verified by default, then results are emitted to console, JSON, SARIF, and the browser UI.
|
||||
|
||||
Detector families: taint (cross-file source→sink, with cap-specific rule classes for SQLi, XSS, command/code exec, deserialization, SSRF, path traversal, format string, crypto, LDAP injection, XPath injection, HTTP header / response splitting, open redirect, server-side template injection, XXE, prototype pollution, data exfiltration, and the auth fold-in), CFG structural (auth gaps, unguarded sinks, resource leaks), state model (use-after-close, double-close, must-leak, unauthed-access), AST patterns (tree-sitter structural match). Full detector docs: [Detectors](https://nyxscan.dev/docs/detectors.html).
|
||||
|
||||
|
|
@ -213,7 +214,7 @@ nyx scan --no-verify # static analysis only, for fast local loops
|
|||
|
||||
A finding is **Confirmed** only when an attacker-controlled payload fires the sink *and* a paired benign control stays clean. That differential rule, plus behavioral oracles (a template that renders `49`, a deserializer that resolves a gadget class, a redirect that leaves the origin), keeps the verifier from confirming on an echoed string. Sinks behind a recognized guard demote to `ConfirmedWithKnownGuard`; sinks reached without a completed exploit chain land as `PartiallyConfirmed`.
|
||||
|
||||
Coverage spans 18 capability classes and 130+ framework adapters across all ten languages (Flask, Django, Express, NestJS, Spring, Rails, Laravel, Gin, Axum, and more), with per-language build pools and copy-on-write workdirs to keep the per-finding cost low. Confirmed findings write a hermetic repro bundle with a `reproduce.sh`. Runs are deterministic: every payload is seeded from the spec hash.
|
||||
Coverage spans 18 verifiable capability classes and 120+ registered adapters across all ten languages (Flask, Django, Express, NestJS, Spring, Rails, Laravel, Gin, Axum, and more), with per-language build pools and copy-on-write workdirs to keep the per-finding cost low. Confirmed findings write a hermetic repro bundle with a `reproduce.sh`. Runs are deterministic: every payload is seeded from the spec hash.
|
||||
|
||||
```bash
|
||||
# CI: fail the build if a new Confirmed finding appears vs. a baseline
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div align="center">
|
||||
<img src="assets/nyx-readme-header.png" alt="NYX" width="640"/>
|
||||
|
||||
**本地优先的安全扫描器,自带浏览器 UI。在本地扫描代码仓库并在浏览器中分诊处理,无需云端、无需账号。**
|
||||
**本地优先的安全扫描器,带沙箱动态验证和浏览器 UI。在本地扫描代码仓库并在浏览器中分诊处理,无需云端、无需账号。**
|
||||
|
||||
[](https://crates.io/crates/nyx-scanner)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
## 本地扫描,本地浏览
|
||||
|
||||
Nyx 在你的代码仓库上运行跨语言污点分析,然后将结果通过绑定到 `127.0.0.1` 的 React UI 提供给你。你会得到一份带严重等级、证据、以及分步**流可视化**的发现列表,从源 → 净化器 → 汇逐步呈现数据流。分诊决策持久化在 `.nyx/triage.json` 中,与代码一同提交,团队共享同一份分诊状态。
|
||||
Nyx 在你的代码仓库上运行跨语言污点分析,然后对中高置信度发现运行小型沙箱 harness,验证真实代码里 source 到 sink 的流是否会触发。结果通过绑定到 `127.0.0.1` 的 React UI 提供给你。你会看到严重等级、静态证据、动态验证结果,以及分步**流可视化**,从源 → 净化器 → 汇逐步呈现数据流。分诊决策持久化在 `.nyx/triage.json` 中,与代码一同提交,团队共享同一份分诊状态。
|
||||
|
||||
```bash
|
||||
cargo install nyx-scanner
|
||||
|
|
@ -26,7 +26,7 @@ nyx scan # 运行分析器,把发现缓存到 .nyx/
|
|||
nyx serve # 在浏览器中打开 http://localhost:9700
|
||||
```
|
||||
|
||||
一切都留在你本地:仅回环绑定、强制 host 头校验、所有变更操作均带 CSRF、无遥测、无登录。
|
||||
一切都留在你本地:仅回环绑定、强制 host 头校验、所有变更操作均带 CSRF、无远程遥测、无登录。
|
||||
|
||||
<p align="center"><img src="assets/screenshots/overview.png" alt="一个小型 JS 应用的总览仪表盘:健康分 C 78,五项分量分解(严重度压力、置信度质量、趋势、分诊覆盖、回归抗性),3 条发现,OWASP A03 与 A02 类别,置信度分布与问题类别条形图,受影响最多的文件" width="900"/></p>
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ nyx serve # 在浏览器中打开 http://localhost:9700
|
|||
|---|---|
|
||||
| **总览** | 仪表盘:按严重等级分类的发现计数、热点文件、引擎画像摘要 |
|
||||
| **发现** | 可浏览列表,含严重度徽章、分诊状态、规则筛选、语言筛选 |
|
||||
| **发现详情** | 流路径可视化,带编号步骤(源 → 净化器 → 汇)、代码片段、证据、跨文件标记、分诊下拉框 |
|
||||
| **发现详情** | 流路径可视化,带编号步骤(源 → 净化器 → 汇)、动态验证结果、代码片段、证据、跨文件标记、分诊下拉框 |
|
||||
| **分诊** | 批量更新状态(open、investigating、fixed、false_positive、accepted_risk、suppressed),审计日志,JSON 导入/导出 |
|
||||
| **资源管理器** | 文件树,含每个文件的符号列表与发现叠加层 |
|
||||
| **扫描** | 历史记录、指标,对比两次扫描查看差异 |
|
||||
|
|
@ -76,7 +76,7 @@ nyx scan --engine-profile deep
|
|||
### GitHub Action
|
||||
|
||||
```yaml
|
||||
- uses: elicpeter/nyx@v0.7.0
|
||||
- uses: elicpeter/nyx@v0.8.0
|
||||
with:
|
||||
format: sarif
|
||||
fail-on: MEDIUM
|
||||
|
|
@ -180,12 +180,25 @@ cd nyx && cargo build --release
|
|||
1. **Pass 1**:用 tree-sitter 解析每个文件,构建过程内 CFG(petgraph),下降到剪枝后的 SSA(在支配边界上做 Cytron phi 插入),并导出每函数摘要(source/sanitizer/sink 能力位、污点变换、指向集、被调集合)。
|
||||
2. **摘要合并**:将每文件摘要并集合并为 `GlobalSummaries` 映射。
|
||||
3. **Pass 2**:在跨文件上下文与有限上下文敏感(文件内被调用 k=1 内联,SCC 不动点上限 64 次迭代,超过内联体大小阈值的被调用走摘要回退)下重新分析每个文件。正向数据流工作表通过 SSA 格传播污点,保证收敛。调用图 SCC 迭代到不动点(在上限内),使相互递归函数能拿到准确摘要。
|
||||
4. **排序、去重、输出**:按 严重度 × 证据强度 × 源类可利用性 打分,并输出到控制台、JSON 或 SARIF。
|
||||
4. **排序、去重、动态验证、输出**:按 严重度 × 证据强度 × 源类可利用性 打分。默认构建会对中高置信度发现做动态验证,然后输出到控制台、JSON、SARIF 和浏览器 UI。
|
||||
|
||||
检测器家族:污点(跨文件 source→sink,含 SQLi、XSS、命令/代码执行、反序列化、SSRF、路径穿越、格式串、加密、LDAP 注入、XPath 注入、HTTP 头/响应拆分、开放重定向、服务端模板注入、XXE、原型污染、数据外泄、以及 auth 折入的能力位类规则)、CFG 结构(鉴权缺失、未守卫汇、资源泄漏)、状态模型(use-after-close、double-close、must-leak、unauthed-access)、AST 模式(tree-sitter 结构匹配)。完整检测器文档:[Detectors](https://nyxscan.dev/docs/detectors.html)。
|
||||
|
||||
---
|
||||
|
||||
## 动态验证
|
||||
|
||||
静态分析说明 source 到 sink 可达。动态验证会尝试证明这条路径在真实代码里会触发。默认构建开启该功能,`nyx scan` 会为中高置信度发现生成 harness,在沙箱中用 curated payload 运行,并把结果写入 `evidence.dynamic_verdict`。
|
||||
|
||||
```bash
|
||||
nyx scan --verify # 默认行为的显式写法
|
||||
nyx scan --no-verify # 只跑静态分析,适合本地快速循环
|
||||
```
|
||||
|
||||
`Confirmed` 只有在攻击 payload 触发 sink 且对应的良性 control 保持干净时才会出现。`NotConfirmed` 表示 harness 跑完但没有触发,不等于发现已关闭。完整能力矩阵、后端与限制见 [Dynamic verification](https://nyxscan.dev/docs/dynamic.html)。
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
配置由 `nyx.conf`(默认值)与 `nyx.local`(你的覆写)合并而成,从平台配置目录读取(Linux 为 `~/.config/nyx/`,macOS 为 `~/Library/Application Support/nyx/`,Windows 为 `%APPDATA%\elicpeter\nyx\config\`)。
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ flowchart TD
|
|||
Pass2 --> Calls["Call precision<br/>k=1 inline, summaries, SCC fixed-point"]
|
||||
Taint --> Findings["Findings with evidence<br/>source, path, sink, engine notes"]
|
||||
Calls --> Findings
|
||||
Findings --> Emit["Rank, dedupe, emit<br/>console, JSON, SARIF, UI"]
|
||||
Findings --> Rank["Rank and dedupe<br/>severity, confidence, score"]
|
||||
Rank --> Verify["Dynamic verification<br/>sandboxed harnesses, verdicts"]
|
||||
Verify --> Emit["Emit<br/>console, JSON, SARIF, UI"]
|
||||
```
|
||||
|
||||
**Pass 1, per file.** Tree-sitter parses the file. Nyx builds an intra-procedural control-flow graph, lowers it to SSA, and extracts a summary per function describing what that function does at the boundary: which arguments flow to sinks, which sources it reads from, which sinks it calls, what taint it strips, what it returns. Summaries are persisted to SQLite ([`src/summary/`](https://github.com/elicpeter/nyx/tree/master/src/summary/), [`src/database.rs`](https://github.com/elicpeter/nyx/blob/master/src/database.rs)).
|
||||
|
|
@ -33,6 +35,8 @@ When a method call has a receiver typed as a super-class, trait, or interface, *
|
|||
|
||||
A separate **field-sensitive points-to** pass tracks abstract locations down to the field level, so `c.mu.Lock()` is a lock on `Field(c, mu)` rather than on `c` as a whole. That distinction is what lets the resource-lifecycle and taint passes tell `obj.field = tainted; sink(obj.other_field)` apart from the conservative whole-variable approximation. Subscript reads and writes (`arr[i]`, `map[k] = v`) lower to synthetic `__index_get__` / `__index_set__` calls so the same container model handles them. Set `NYX_POINTER_ANALYSIS=0` to fall back to the pre-pointer-pass behaviour for baseline comparison.
|
||||
|
||||
**Dynamic verification.** After ranking and dedupe, default builds verify Medium and High confidence findings unless `--no-verify` or `scanner.verify = false` is set. The verifier derives a small harness from the finding, runs it in a sandbox against curated payloads, and stores the result on `evidence.dynamic_verdict`. `Confirmed` means a vulnerable payload fired and its benign control stayed clean. `NotConfirmed` means the harness ran but did not fire, not that the finding is closed.
|
||||
|
||||
## Optional analyses on top
|
||||
|
||||
These run on top of the forward taint pass. They're independently switchable via `[analysis.engine]` config or matching CLI flags. See [advanced-analysis.md](advanced-analysis.md) for the full description and tradeoffs.
|
||||
|
|
@ -62,6 +66,6 @@ Findings whose engine notes indicate a bound was hit can be filtered with `--req
|
|||
|
||||
## What you get out
|
||||
|
||||
Each finding carries the source location, the sink location, the path in between (when symex produced one), the rule ID, severity, attack-surface score, confidence level, and a list of engine notes describing any precision loss along the way. Console output is human-readable; JSON and SARIF carry the full evidence object for tooling.
|
||||
Each finding carries the source location, the sink location, the path in between (when symex produced one), the rule ID, severity, attack-surface score, confidence level, dynamic verdict when one was attempted, and a list of engine notes describing any precision loss along the way. Console output is human-readable; JSON and SARIF carry the full evidence object for tooling.
|
||||
|
||||
For the JSON shape and SARIF mapping, see [output.md](output.md).
|
||||
|
|
|
|||
134
docs/output.md
134
docs/output.md
|
|
@ -69,48 +69,71 @@ Use --include-quality, --max-low, or --all to adjust.
|
|||
|
||||
## JSON
|
||||
|
||||
Machine-readable JSON array. Each finding is an object:
|
||||
Machine-readable JSON object. The main keys are:
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `findings` | array | Finding objects |
|
||||
| `chains` | array | Composed exploit chains, when emitted |
|
||||
| `dynamic_verification` | object | Count of attached dynamic verdicts |
|
||||
| `verdict_diff` | object | Baseline comparison, only when `--baseline` is used |
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"path": "src/handler.rs",
|
||||
"line": 12,
|
||||
"col": 5,
|
||||
"severity": "High",
|
||||
"id": "taint-unsanitised-flow (source 5:11)",
|
||||
"path_validated": false,
|
||||
"labels": [
|
||||
["Source", "env::var(\"CMD\") at 5:11"],
|
||||
["Sink", "Command::new(\"sh\").arg(\"-c\")"]
|
||||
],
|
||||
"confidence": "High",
|
||||
"evidence": {
|
||||
"source": {
|
||||
"path": "src/handler.rs",
|
||||
"line": 5,
|
||||
"col": 11,
|
||||
"kind": "source",
|
||||
"snippet": "env::var(\"CMD\")"
|
||||
{
|
||||
"findings": [
|
||||
{
|
||||
"path": "src/handler.rs",
|
||||
"line": 12,
|
||||
"col": 5,
|
||||
"severity": "High",
|
||||
"id": "taint-unsanitised-flow (source 5:11)",
|
||||
"path_validated": false,
|
||||
"labels": [
|
||||
["Source", "env::var(\"CMD\") at 5:11"],
|
||||
["Sink", "Command::new(\"sh\").arg(\"-c\")"]
|
||||
],
|
||||
"confidence": "High",
|
||||
"evidence": {
|
||||
"source": {
|
||||
"path": "src/handler.rs",
|
||||
"line": 5,
|
||||
"col": 11,
|
||||
"kind": "source",
|
||||
"snippet": "env::var(\"CMD\")"
|
||||
},
|
||||
"sink": {
|
||||
"path": "src/handler.rs",
|
||||
"line": 12,
|
||||
"col": 5,
|
||||
"kind": "sink",
|
||||
"snippet": "Command::new(\"sh\")"
|
||||
},
|
||||
"notes": ["source_kind:EnvironmentConfig"],
|
||||
"dynamic_verdict": {
|
||||
"finding_id": "a3b12f0c91e04420",
|
||||
"status": "Confirmed",
|
||||
"triggered_payload": "cmdi-echo-marker"
|
||||
}
|
||||
},
|
||||
"sink": {
|
||||
"path": "src/handler.rs",
|
||||
"line": 12,
|
||||
"col": 5,
|
||||
"kind": "sink",
|
||||
"snippet": "Command::new(\"sh\")"
|
||||
},
|
||||
"notes": ["source_kind:EnvironmentConfig"]
|
||||
},
|
||||
"rank_score": 76.0,
|
||||
"rank_reason": [
|
||||
["severity_base", "60"],
|
||||
["analysis_kind", "10"],
|
||||
["source_kind", "5"],
|
||||
["evidence_count", "1"]
|
||||
]
|
||||
"rank_score": 76.0,
|
||||
"rank_reason": [
|
||||
["severity_base", "60"],
|
||||
["analysis_kind", "10"],
|
||||
["source_kind", "5"],
|
||||
["evidence_count", "1"]
|
||||
]
|
||||
}
|
||||
],
|
||||
"chains": [],
|
||||
"dynamic_verification": {
|
||||
"total": 1,
|
||||
"confirmed": 1,
|
||||
"partially_confirmed": 0,
|
||||
"not_confirmed": 0,
|
||||
"inconclusive": 0,
|
||||
"unsupported": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Field descriptions
|
||||
|
|
@ -132,6 +155,7 @@ Machine-readable JSON array. Each finding is an object:
|
|||
| `rank_score` | float | no | Attack-surface score (omitted when ranking disabled) |
|
||||
| `rank_reason` | array | no | Score breakdown (omitted when ranking disabled) |
|
||||
| `rollup` | object | no | Rollup data when findings are grouped (see below) |
|
||||
| `chain_member_of` | int | no | Stable hash of the emitted chain this finding belongs to |
|
||||
|
||||
Fields marked "no" are omitted when empty/null/false to keep output compact.
|
||||
|
||||
|
|
@ -155,9 +179,40 @@ The `evidence` field provides structured provenance data:
|
|||
| `sanitizers` | array | Sanitizer spans |
|
||||
| `state` | object | State-machine evidence (machine, subject, from_state, to_state) |
|
||||
| `notes` | array | Free-form notes (e.g. `"source_kind:UserInput"`, `"path_validated"`) |
|
||||
| `dynamic_verdict` | object | Dynamic verification result, when verification ran or was skipped for a typed reason |
|
||||
|
||||
All fields are omitted when empty/null.
|
||||
|
||||
### Dynamic verdict object
|
||||
|
||||
`evidence.dynamic_verdict` uses this shape:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `finding_id` | string | Stable 16-character hex finding id |
|
||||
| `status` | string | `Confirmed`, `PartiallyConfirmed`, `NotConfirmed`, `Inconclusive`, or `Unsupported` |
|
||||
| `triggered_payload` | string | Payload label for `Confirmed` verdicts |
|
||||
| `reason` | object/string | Typed reason for `Unsupported` |
|
||||
| `inconclusive_reason` | object/string | Typed reason for `Inconclusive` |
|
||||
| `detail` | string | Extra build, sandbox, or policy detail |
|
||||
| `attempts` | array | Per-payload attempt summaries |
|
||||
| `toolchain_match` | string | `exact` or `drift` |
|
||||
| `differential` | object | Vulnerable versus benign control result, when both ran |
|
||||
| `hardening_outcome` | object | Process-backend hardening result, when recorded |
|
||||
|
||||
The top-level `dynamic_verification` object counts verdict statuses across the emitted findings:
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 4,
|
||||
"confirmed": 2,
|
||||
"partially_confirmed": 0,
|
||||
"not_confirmed": 1,
|
||||
"inconclusive": 0,
|
||||
"unsupported": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Rollup object
|
||||
|
||||
When a finding is a rollup (grouped from multiple occurrences), the `rollup` field is present:
|
||||
|
|
@ -195,7 +250,8 @@ The SARIF output includes:
|
|||
- **Tool metadata**: Nyx name and version
|
||||
- **Rules**: Rule ID, description, severity mapping
|
||||
- **Results**: One result per finding with location, message, and properties
|
||||
- **Properties**: Each result includes `category` and optionally `confidence` and `rollup.count`
|
||||
- **Properties**: Each result includes `category` and optionally `confidence`, `rollup.count`, and `nyx_dynamic_verdict`
|
||||
- **Fingerprints**: Dynamic verdict status is added as `partialFingerprints.dynamic_verdict_status` when present
|
||||
- **Related locations**: Rollup findings include example locations in `relatedLocations`
|
||||
- **Artifacts**: File paths referenced by findings
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ After `cargo install nyx-scanner` (or dropping a release binary on your PATH), p
|
|||
nyx scan ./my-project
|
||||
```
|
||||
|
||||
First run builds a SQLite index under `.nyx/`; later runs skip files whose content hash hasn't changed.
|
||||
First run builds a SQLite index under `.nyx/`; later runs skip files whose content hash hasn't changed. Default builds also verify Medium and High confidence findings in a sandbox. Use `--no-verify` when you want a static-only local loop.
|
||||
|
||||
## What a finding looks like
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ The same scan in console form:
|
|||
|
||||
Source: request.args.get (5:11)
|
||||
Sink: os.system
|
||||
[DYN: confirmed via cmdi-echo-marker-python]
|
||||
|
||||
6:5 ✖ [HIGH] py.cmdi.os_system (Score: 64, Confidence: High)
|
||||
os.system() runs a shell command
|
||||
|
|
@ -31,12 +32,15 @@ The same scan in console form:
|
|||
|
||||
Source: req.query.content (3:18)
|
||||
Sink: document.write
|
||||
[DYN: confirmed via xss-script-marker]
|
||||
|
||||
5:5 ⚠ [MEDIUM] js.xss.document_write (Score: 34, Confidence: High)
|
||||
document.write() is an XSS sink
|
||||
|
||||
Dynamic verification: 4 verdicts (2 confirmed, 0 partially confirmed, 1 not confirmed, 0 inconclusive, 1 unsupported)
|
||||
|
||||
warning 'demo' generated 10 issues.
|
||||
Finished in 0.054s.
|
||||
Finished in 1.842s.
|
||||
```
|
||||
|
||||
Each finding is one line of header plus evidence. Fields that matter:
|
||||
|
|
@ -48,6 +52,7 @@ Each finding is one line of header plus evidence. Fields that matter:
|
|||
| Score | Attack-surface ranking (severity + analysis kind + source kind + evidence). Higher is more exploitable |
|
||||
| Confidence | `High`, `Medium`, `Low`. Drops for AST-only matches, capped widened flows, and lowered-to-Low backwards-infeasible findings |
|
||||
| Source / Sink | Where tainted data entered and where the dangerous call happened |
|
||||
| `[DYN: ...]` | Dynamic verifier result, when Nyx built and ran a harness for the finding |
|
||||
|
||||
Two rules firing on the same line (the taint finding plus the AST pattern) is normal. The pattern matches the structural presence of `document.write`; the taint rule adds the evidence that `req.query.content` actually reached it. Both carry distinct rule IDs so suppressions can target one without the other.
|
||||
|
||||
|
|
@ -85,14 +90,17 @@ nyx scan . --require-converged
|
|||
|
||||
`--require-converged` keeps `under-report` findings (the emitted flow is still real) but drops over-reports and widenings. Intended for strict gates where a noisy finding is worse than nothing.
|
||||
|
||||
## Skip dataflow for a fast first pass
|
||||
## Skip work for a fast first pass
|
||||
|
||||
```bash
|
||||
nyx scan . --mode ast
|
||||
nyx scan . --no-verify
|
||||
```
|
||||
|
||||
AST-only mode runs tree-sitter patterns without building a CFG or running taint. It's fast and still catches banned-API uses, weak crypto, and obvious XSS sinks, but it can't tell `eval("1+1")` apart from `eval(userInput)`. Use it as a pre-commit filter, not as a CI gate replacement.
|
||||
|
||||
`--no-verify` keeps the static engine on but skips sandboxed execution. Use it when you are iterating locally and only need the analyzer result.
|
||||
|
||||
## Next
|
||||
|
||||
- [CLI reference](cli.md) for every flag and subcommand.
|
||||
|
|
|
|||
|
|
@ -247,6 +247,18 @@ mod hardening_tests {
|
|||
// that graft does not land on an unprivileged-userns host the line is
|
||||
// missing through no fault of the prctl call (recorded Applied in the
|
||||
// outcome) — skip rather than fail, matching the seccomp test.
|
||||
// A transient reap on a locked-down host can leave the probe's
|
||||
// (unbuffered) stdout empty/partial before the sentinel; that is an
|
||||
// environment limitation, not a prctl regression (the primitive is
|
||||
// recorded on the status pipe regardless). Skip when the probe never
|
||||
// ran to completion, matching `probe_runs_under_strict_profile`.
|
||||
if !stdout.contains("__NYX_PROBE_DONE__") {
|
||||
eprintln!(
|
||||
"SKIP: the probe did not run to completion under Strict (transient reap \
|
||||
on a locked-down host); PR_SET_NO_NEW_PRIVS still ran. stdout:\n{stdout}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if chrooted_probe_line_unreliable(&result, &stdout, "NoNewPrivs:\t1") {
|
||||
eprintln!(
|
||||
"SKIP: chroot applied but the chrooted /proc/self/status was unreadable \
|
||||
|
|
@ -271,15 +283,17 @@ mod hardening_tests {
|
|||
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
|
||||
let stdout = stdout_string(&result);
|
||||
// The rlimit lines come from `getrlimit(2)`, not `/proc`, so they print
|
||||
// whenever the probe runs to completion. Under Strict+chroot the probe
|
||||
// can die before flushing its buffered stdout when the best-effort
|
||||
// `/proc` graft does not land — coming back empty through no fault of
|
||||
// the setrlimit call. Skip when chroot relocated the probe and the run
|
||||
// never reached its `__NYX_PROBE_DONE__` sentinel.
|
||||
if chrooted_probe_line_unreliable(&result, &stdout, "__NYX_PROBE_DONE__") {
|
||||
// whenever the probe runs to completion. Under Strict the probe can be
|
||||
// reaped before flushing its (unbuffered) stdout — a transient on a
|
||||
// locked-down host (AppArmor-restricted userns), or a chrooted probe
|
||||
// whose best-effort `/proc` graft did not land — coming back empty
|
||||
// through no fault of the setrlimit call. Skip when the run never
|
||||
// reached its `__NYX_PROBE_DONE__` sentinel.
|
||||
if !stdout.contains("__NYX_PROBE_DONE__") {
|
||||
eprintln!(
|
||||
"SKIP: chroot applied but the probe produced no sentinel (the /proc graft \
|
||||
did not land on this host); the RLIMIT_CPU cap itself still applied. \
|
||||
"SKIP: the probe produced no completion sentinel under Strict (a transient \
|
||||
reap on a locked-down host, or a chrooted probe whose best-effort /proc \
|
||||
graft did not land); the RLIMIT_CPU cap itself still applied. \
|
||||
stdout:\n{stdout}"
|
||||
);
|
||||
return;
|
||||
|
|
@ -311,10 +325,11 @@ mod hardening_tests {
|
|||
// (best-effort `/proc` graft missed on an unprivileged-userns host).
|
||||
// The cap itself applied; skip rather than fail. See
|
||||
// `chrooted_probe_line_unreliable`.
|
||||
if chrooted_probe_line_unreliable(&result, &stdout, "__NYX_PROBE_DONE__") {
|
||||
if !stdout.contains("__NYX_PROBE_DONE__") {
|
||||
eprintln!(
|
||||
"SKIP: chroot applied but the probe produced no sentinel (the /proc graft \
|
||||
did not land on this host); the RLIMIT_NOFILE cap itself still applied. \
|
||||
"SKIP: the probe produced no completion sentinel under Strict (a transient \
|
||||
reap on a locked-down host, or a chrooted probe whose best-effort /proc \
|
||||
graft did not land); the RLIMIT_NOFILE cap itself still applied. \
|
||||
stdout:\n{stdout}"
|
||||
);
|
||||
return;
|
||||
|
|
@ -342,10 +357,11 @@ mod hardening_tests {
|
|||
// the chrooted probe never flushed (best-effort `/proc` graft missed on
|
||||
// an unprivileged-userns host). The cap itself applied; skip rather
|
||||
// than fail. See `chrooted_probe_line_unreliable`.
|
||||
if chrooted_probe_line_unreliable(&result, &stdout, "__NYX_PROBE_DONE__") {
|
||||
if !stdout.contains("__NYX_PROBE_DONE__") {
|
||||
eprintln!(
|
||||
"SKIP: chroot applied but the probe produced no sentinel (the /proc graft \
|
||||
did not land on this host); the RLIMIT_AS cap itself still applied. \
|
||||
"SKIP: the probe produced no completion sentinel under Strict (a transient \
|
||||
reap on a locked-down host, or a chrooted probe whose best-effort /proc \
|
||||
graft did not land); the RLIMIT_AS cap itself still applied. \
|
||||
stdout:\n{stdout}"
|
||||
);
|
||||
return;
|
||||
|
|
@ -510,6 +526,32 @@ mod hardening_tests {
|
|||
|
||||
match outcome.seccomp {
|
||||
PrimitiveStatus::Applied => {
|
||||
// The `Seccomp:\t2` line is a *secondary* cross-check: the
|
||||
// authoritative "filter installed" signal is
|
||||
// `outcome.seccomp == Applied`, which the child wrote to the
|
||||
// status pipe in pre_exec *before* execve — independent of
|
||||
// whether the probe's stdout ever made it back. The probe's
|
||||
// stdout is only a trustworthy witness when the probe ran to
|
||||
// completion (its `__NYX_PROBE_DONE__` sentinel is present).
|
||||
// On a locked-down CI runner the Strict sequence is degraded
|
||||
// (AppArmor-restricted unprivileged userns fails unshare +
|
||||
// chroot) and the probe can be reaped transiently before its
|
||||
// (unbuffered) stdout completes, coming back empty/partial.
|
||||
// That empty run is an environment limitation, not a seccomp
|
||||
// regression — skip, exactly as `probe_runs_under_strict_profile`
|
||||
// does for the same transient. This generalises the older
|
||||
// chroot-only gate below, which only covered the
|
||||
// chroot-relocated case and let the chroot-*failed* transient
|
||||
// (no /proc graft involved) fall through to a spurious assert.
|
||||
if !stdout.contains("__NYX_PROBE_DONE__") {
|
||||
eprintln!(
|
||||
"SKIP: the probe did not run to completion under Strict (empty or \
|
||||
partial stdout from a transient reap on a locked-down host); the \
|
||||
seccomp install itself reported Applied on the status pipe \
|
||||
independent of the probe's stdout. stdout:\n{stdout}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
// The probe can only read `Seccomp:\t2` from its own
|
||||
// `/proc/self/status`. Under Strict+chroot with no host-lib
|
||||
// bind (strict_opts keeps `bind_mount_host_libs=false`), the
|
||||
|
|
@ -519,11 +561,11 @@ mod hardening_tests {
|
|||
// bind result is intentionally ignored), leaving
|
||||
// `<workdir>/proc` empty and `/proc/self/status` unreadable.
|
||||
// In that case the probe prints the `Seccomp:\t?` fallback
|
||||
// through no fault of the seccomp install itself — which the
|
||||
// kernel already confirmed via `outcome.seccomp == Applied`.
|
||||
// Only require the line when the line's source (a real /proc)
|
||||
// was reachable, i.e. when chroot did NOT relocate the probe
|
||||
// onto the graft.
|
||||
// (still followed by the sentinel) through no fault of the
|
||||
// seccomp install itself — which the kernel already confirmed
|
||||
// via `outcome.seccomp == Applied`. Only require the line when
|
||||
// the line's source (a real /proc) was reachable, i.e. when
|
||||
// chroot did NOT relocate the probe onto the graft.
|
||||
if matches!(outcome.chroot, PrimitiveStatus::Applied)
|
||||
&& !stdout.contains("Seccomp:\t2")
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue