Skip to content
Merged
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
570 changes: 570 additions & 0 deletions coverage.out

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,43 @@ report.md readable summary

The manifest distinguishes the `completed`, `partial` and `failed` states,
references the produced artifacts and keeps the scope source for audit.

## Triage layer (H3.1)

`scanforge triage <run>` derives interpretation from the report without ever
modifying it:

```text
report.json ──► finding.FromReport ──► canonical findings (deterministic IDs)
finding.BuildRelations (L0/L1)
triage engine: group → bundle → analyze → validate
<run>/triage/ (manifest, relations, insights, report.md)
```

The boundary is strict: **ScanForge owns facts, AI owns interpretations,
validation sits between them.**

- `internal/finding` projects the report into flat findings with
deterministic IDs (`F-` + hash of source|template|asset|matched_at|evidence)
and computes deterministic relations (duplicate 1.00, shared CVE 0.99,
same endpoint 0.95, same asset 0.80). L2 (semantic) relations can add to
them but never override them.
- `internal/triage` runs the pipeline: grouping (union-find over the relation
graph), deterministic insights (summary + duplicate groups), optional LLM
analysis, validation and reconciliation (priority-ordered, stable IDs).
- The LLM receives only a reduced projection (`TriageBundle`): truncated
evidence, no raw tool output, capped at 150 findings. Its output is
validated against the facts — unknown finding IDs, CVEs or evidence strings
reject the whole insight — so the model cannot inject new truths.
- `internal/inference` abstracts the transport behind a `Client` interface;
the bundled implementation speaks the OpenAI-compatible chat completions
API (llama.cpp, vLLM, Ollama, ...).
- Provenance is recorded in `triage/manifest.json` (model, prompt version,
input digest, temperature) and the cache reuses results when the input
digest, model and prompt version are unchanged (`--force` bypasses it).
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ continuité (diff entre runs, scheduling, notifications).

### H3 — Vision

| # | Idée |
|---|---|
| H3.1 | Triage IA des findings : résumé LLM + déduplication |
| H3.2 | Report HTML type nuclei (compte rendu client) |
| H3.3 | Données live : fetch EPSS/KEV/NVD à jour plutôt que dataset embarqué |
| # | Idée | Statut |
|---|---|---|
| H3.1 | Triage IA des findings : résumé LLM + déduplication | ✅ implémenté |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| H3.2 | Report HTML type nuclei (compte rendu client) | |
| H3.3 | Données live : fetch EPSS/KEV/NVD à jour plutôt que dataset embarqué | |

## 4. Hors périmètre (anti-scope creep)

Expand Down
41 changes: 41 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ module_timeouts:
katana: 20m
```

The `ai` section configures the LLM backend used by `scanforge triage`. Any
server exposing the OpenAI-compatible chat completions API works (llama.cpp,
vLLM, Ollama, LM Studio, ...). When the section is omitted, triage runs in
deterministic-only mode (deduplication and grouping without a model):

```yaml
ai:
base_url: http://127.0.0.1:8080/v1
model: qwen3.5-9b
api_key: "" # optional for local servers
timeout: 5m
temperature: 0.1 # low values keep triage output stable
```

## Built-in nuclei templates

`--nuclei-include-custom` adds the templates bundled in the `templates/`
Expand Down Expand Up @@ -124,11 +138,38 @@ scanforge run example.com --scope-mode domain --confirm-scope
| `scanforge scan TARGET` | Alias of `run`. |
| `scanforge diff RUN1 RUN2` | Delta (assets/ports/vulns) between two runs of the same target. |
| `scanforge export RUN --format sarif\|defectdojo` | Exports a run report for CI (SARIF) or DefectDojo (generic findings). |
| `scanforge triage RUN` | Groups, groups and (with an `ai:` backend) analyzes the findings of a run. |
| `scanforge auth` | Manages the keys required by some tools. |
| `scanforge version` | Displays the binary version. |

See `scanforge <command> --help` for the exact list of options.

## Triage of findings

`scanforge triage <run>` projects the consolidated report into canonical
findings, computes deterministic relations (duplicates, shared CVE, shared
endpoint, same asset) and writes the result under `<run>/triage/`:

```text
triage/manifest.json provenance: model, prompt version, input digest
triage/relations.json deterministic finding-to-finding relations
triage/insights.json insights (dedup groups + validated LLM insights)
triage/report.md human-readable summary
```

With an `ai:` backend configured, the model receives a deliberately reduced
projection of the findings (truncated evidence, no raw tool output) and its
insights are validated before being stored: any insight referencing an
unknown finding ID, CVE or evidence string is rejected. The model can
interpret findings, never create them. Re-running with unchanged input hits
the cache (0 inference); `--force` bypasses it:

```bash
scanforge triage runs/example.com/2026-08-19T10:00:00Z
scanforge triage runs/example.com/2026-08-19T10:00:00Z --force
scanforge triage runs/example.com/2026-08-19T10:00:00Z --model qwen3.5-9b
```

## Multi-target engagements

`run` and `plan` accept a targets file instead of a single positional target.
Expand Down
43 changes: 43 additions & 0 deletions docs/fr/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,46 @@ report.md synthèse lisible

Le manifeste distingue les états `completed`, `partial` et `failed`, référence
les artefacts produits et conserve la source du scope pour audit.

## Couche triage (H3.1)

`scanforge triage <run>` dérive une interprétation du rapport sans jamais le
modifier :

```text
report.json ──► finding.FromReport ──► findings canoniques (IDs déterministes)
finding.BuildRelations (L0/L1)
moteur triage : group → bundle → analyze → validate
<run>/triage/ (manifest, relations, insights, report.md)
```

La frontière est stricte : **ScanForge possède les faits, l'IA possède les
interprétations, la validation se tient entre les deux.**

- `internal/finding` projette le rapport en findings plats avec IDs
déterministes (`F-` + hash de source|template|asset|matched_at|evidence) et
calcule les relations déterministes (doublon 1.00, CVE partagée 0.99, même
endpoint 0.95, même actif 0.80). Les relations sémantiques (L2) peuvent s'y
ajouter mais jamais les surcharger.
- `internal/triage` exécute le pipeline : regroupement (union-find sur le
graphe de relations), insights déterministes (résumé + groupes de doublons),
analyse LLM optionnelle, validation et réconciliation (tri par priorité, IDs
stables).
- Le LLM ne reçoit qu'une projection réduite (`TriageBundle`) : preuves
tronquées, jamais de sortie brute d'outil, plafonnée à 150 findings. Sa
sortie est validée contre les faits — un ID de finding, une CVE ou une
preuve inconnus rejettent l'insight entier — donc le modèle ne peut pas
injecter de nouvelles vérités.
- `internal/inference` abstrait le transport derrière une interface `Client` ;
l'implémentation livrée parle l'API OpenAI-compatible chat completions
(llama.cpp, vLLM, Ollama, ...).
- La provenance est enregistrée dans `triage/manifest.json` (modèle, version
du prompt, empreinte d'entrée, température) et le cache réutilise les
résultats quand l'empreinte d'entrée, le modèle et la version du prompt sont
inchangés (`--force` le contourne).
10 changes: 5 additions & 5 deletions docs/fr/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ continuité (diff entre runs, scheduling, notifications).

### H3 — Vision

| # | Idée |
|---|---|
| H3.1 | Triage IA des findings : résumé LLM + déduplication |
| H3.2 | Report HTML type nuclei (compte rendu client) |
| H3.3 | Données live : fetch EPSS/KEV/NVD à jour plutôt que dataset embarqué |
| # | Idée | Statut |
|---|---|---|
| H3.1 | Triage IA des findings : résumé LLM + déduplication | ✅ implémenté |
| H3.2 | Report HTML type nuclei (compte rendu client) | |
| H3.3 | Données live : fetch EPSS/KEV/NVD à jour plutôt que dataset embarqué | |

## 4. Hors périmètre (anti-scope creep)

Expand Down
42 changes: 42 additions & 0 deletions docs/fr/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ module_timeouts:
katana: 20m
```

La section `ai` configure le backend LLM utilisé par `scanforge triage`. Tout
serveur exposant l'API OpenAI-compatible chat completions fonctionne
(llama.cpp, vLLM, Ollama, LM Studio, ...). Si la section est absente, le
triage fonctionne en mode purement déterministe (déduplication et
regroupement sans modèle) :

```yaml
ai:
base_url: http://127.0.0.1:8080/v1
model: qwen3.5-9b
api_key: "" # facultatif pour les serveurs locaux
timeout: 5m
temperature: 0.1 # des valeurs basses stabilisent le triage
```

## Templates nuclei intégrés

`--nuclei-include-custom` ajoute au run nuclei les templates livrés dans le
Expand Down Expand Up @@ -123,11 +138,38 @@ scanforge run example.com --scope-mode domain --confirm-scope
| `scanforge plan TARGET` | Affiche le scope et les vagues du DAG. |
| `scanforge run TARGET` | Exécute un profil autorisé. |
| `scanforge scan TARGET` | Alias de `run`. |
| `scanforge triage RUN` | Regroupe et (avec un backend `ai:`) analyse les findings d'un run. |
| `scanforge auth` | Gère les clés requises par certains outils. |
| `scanforge version` | Affiche la version du binaire. |

Consultez `scanforge <commande> --help` pour la liste exacte des options.

## Triage des findings

`scanforge triage <run>` projette le rapport consolidé en findings canoniques,
calcule les relations déterministes (doublons, CVE partagée, endpoint commun,
même actif) et écrit le résultat sous `<run>/triage/` :

```text
triage/manifest.json provenance : modèle, version du prompt, empreinte d'entrée
triage/relations.json relations déterministes entre findings
triage/insights.json insights (groupes de doublons + insights LLM validés)
triage/report.md résumé lisible
```

Avec un backend `ai:` configuré, le modèle reçoit une projection volontairement
réduite des findings (preuves tronquées, jamais de sortie brute d'outil) et ses
insights sont validés avant stockage : tout insight référençant un ID de
finding, une CVE ou une preuve inconnus est rejeté. Le modèle peut interpréter
les findings, jamais en créer. Relancer avec une entrée inchangée touche le
cache (0 inférence) ; `--force` le contourne :

```bash
scanforge triage runs/example.com/2026-08-19T10:00:00Z
scanforge triage runs/example.com/2026-08-19T10:00:00Z --force
scanforge triage runs/example.com/2026-08-19T10:00:00Z --model qwen3.5-9b
```

## Engagements multi-cibles

`run` et `plan` acceptent un fichier de cibles au lieu d'une cible positionnelle
Expand Down
33 changes: 33 additions & 0 deletions docs/zh/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,36 @@ report.md 可读摘要
```

清单区分 `completed`、`partial` 和 `failed` 状态,引用已产生的产物,并保留范围来源以供审计。

## 分诊层(H3.1)

`scanforge triage <run>` 从报告中派生解释,而绝不修改报告本身:

```text
report.json ──► finding.FromReport ──► 规范化发现结果(确定性 ID)
finding.BuildRelations(L0/L1)
分诊引擎:group → bundle → analyze → validate
<run>/triage/(manifest、relations、insights、report.md)
```

边界是严格的:**ScanForge 拥有事实,AI 拥有解释,验证位于两者之间。**

- `internal/finding` 将报告投影为扁平发现结果,使用确定性 ID
(`F-` + source|template|asset|matched_at|evidence 的哈希),并计算确定性
关系(重复 1.00、共享 CVE 0.99、相同端点 0.95、相同资产 0.80)。语义关系
(L2)可以补充它们,但绝不能覆盖它们。
- `internal/triage` 运行流水线:分组(对关系图做 union-find)、确定性洞察
(摘要 + 去重组)、可选的 LLM 分析、验证与协调(按优先级排序、ID 稳定)。
- LLM 只收到精简投影(`TriageBundle`):证据被截断、绝不发送工具原始输出、
上限 150 条发现。其输出会对照事实进行验证——未知的发现 ID、CVE 或证据
字符串会拒绝整个洞察——因此模型无法注入新的事实。
- `internal/inference` 将传输抽象为 `Client` 接口;内置实现使用 OpenAI 兼容
chat completions API(llama.cpp、vLLM、Ollama 等)。
- 来源记录在 `triage/manifest.json` 中(模型、提示词版本、输入摘要、温度),
当输入摘要、模型和提示词版本不变时,缓存会复用结果(`--force` 可绕过)。
10 changes: 5 additions & 5 deletions docs/zh/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ ScanForge 是一个 Go CLI,将侦察/安全工具编排为基于产物的流

### H3 — 愿景

| # | 想法 |
|---|---|
| H3.1 | 发现结果 AI 分诊:LLM 摘要 + 去重 |
| H3.2 | 类 nuclei 的 HTML 报告(客户报告) |
| H3.3 | 实时数据:获取最新 EPSS/KEV/NVD,而非内置数据集 |
| # | 想法 | 状态 |
|---|---|---|
| H3.1 | 发现结果 AI 分诊:LLM 摘要 + 去重 | ✅ 已实现 |
| H3.2 | 类 nuclei 的 HTML 报告(客户报告) | |
| H3.3 | 实时数据:获取最新 EPSS/KEV/NVD,而非内置数据集 | |

## 4. 范围外(防止范围蔓延)

Expand Down
37 changes: 37 additions & 0 deletions docs/zh/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ module_timeouts:
katana: 20m
```

`ai` 部分配置 `scanforge triage` 使用的 LLM 后端。任何提供 OpenAI 兼容
chat completions API 的服务器都可以(llama.cpp、vLLM、Ollama、LM Studio 等)。
省略该部分时,triage 以纯确定性模式运行(无模型的去重和分组):

```yaml
ai:
base_url: http://127.0.0.1:8080/v1
model: qwen3.5-9b
api_key: "" # 本地服务器可省略
timeout: 5m
temperature: 0.1 # 低值可保持 triage 输出稳定
```

## 推荐流程

首先检查依赖和计划:
Expand Down Expand Up @@ -107,11 +120,35 @@ scanforge run example.com --scope-mode domain --confirm-scope
| `scanforge plan TARGET` | 显示范围与 DAG 波次。 |
| `scanforge run TARGET` | 运行已授权的配置文件。 |
| `scanforge scan TARGET` | `run` 的别名。 |
| `scanforge triage RUN` | 对一次运行的发现结果去重、分组并(配置 `ai:` 后端时)分析。 |
| `scanforge auth` | 管理某些工具所需的密钥。 |
| `scanforge version` | 显示二进制版本。 |

查看 `scanforge <命令> --help` 获取完整选项列表。

## 发现结果分诊

`scanforge triage <run>` 将合并后的报告投影为规范化的发现结果,计算确定性关系
(重复、共享 CVE、相同端点、相同资产),并将结果写入 `<run>/triage/`:

```text
triage/manifest.json 来源:模型、提示词版本、输入摘要
triage/relations.json 发现结果之间的确定性关系
triage/insights.json 洞察(去重组 + 已验证的 LLM 洞察)
triage/report.md 人类可读摘要
```

配置 `ai:` 后端后,模型只收到刻意精简的发现结果投影(证据截断、绝不发送工具
原始输出),其洞察在存储前会经过验证:任何引用未知发现 ID、CVE 或证据字符串
的洞察都会被拒绝。模型可以解释发现结果,但绝不能创建它们。输入未变化时重新
运行会命中缓存(0 次推理);`--force` 可绕过缓存:

```bash
scanforge triage runs/example.com/2026-08-19T10:00:00Z
scanforge triage runs/example.com/2026-08-19T10:00:00Z --force
scanforge triage runs/example.com/2026-08-19T10:00:00Z --model qwen3.5-9b
```

## 多目标评估

`run` 和 `plan` 接受目标文件,而非单一位置参数目标。每个目标都有独立的范围验证、运行目录和报告(`runs/<target>/`);一个目标失败不会中断其余评估。
Expand Down
19 changes: 19 additions & 0 deletions internal/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net/url"
"os"
"sort"
"strings"
Expand Down Expand Up @@ -68,6 +69,24 @@ func (a *App) ValidateConfig(ctx context.Context) (*ValidateConfigResult, error)
}
}

if cfg.AI.Model != "" || cfg.AI.BaseURL != "" || cfg.AI.APIKey != "" {
if cfg.AI.BaseURL == "" {
result.Problems = append(result.Problems,
"ai.base_url is required when the ai section is configured (e.g. http://127.0.0.1:8080/v1)")
} else if parsed, err := url.Parse(cfg.AI.BaseURL); err != nil || parsed.Hostname() == "" {
result.Problems = append(result.Problems,
fmt.Sprintf("ai.base_url %q is not a valid URL", cfg.AI.BaseURL))
}
if cfg.AI.Model == "" {
result.Problems = append(result.Problems,
"ai.model is required when the ai section is configured")
}
if cfg.AI.Temperature != nil && (*cfg.AI.Temperature < 0 || *cfg.AI.Temperature > 2) {
result.Problems = append(result.Problems,
fmt.Sprintf("ai.temperature %v is out of range (expected 0.0-2.0)", *cfg.AI.Temperature))
}
}

for tool, toolPath := range customToolPaths(cfg) {
if _, err := os.Stat(toolPath); err != nil {
result.Problems = append(result.Problems,
Expand Down
Loading
Loading