wize-dev-kit 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +1 -1
  3. package/src/security-overlay/_shared/allowlist.js +154 -0
  4. package/src/security-overlay/_shared/backlog.js +180 -0
  5. package/src/security-overlay/_shared/cli-runner.js +87 -0
  6. package/src/security-overlay/_shared/cvss.js +108 -0
  7. package/src/security-overlay/_shared/detect.js +125 -0
  8. package/src/security-overlay/_shared/install-script.js +205 -0
  9. package/src/security-overlay/_shared/invoke-phase.js +86 -0
  10. package/src/security-overlay/_shared/owasp.js +56 -0
  11. package/src/security-overlay/_shared/partial.js +225 -0
  12. package/src/security-overlay/_shared/preflight.js +175 -0
  13. package/src/security-overlay/_shared/scope-gate.js +172 -0
  14. package/src/security-overlay/_shared/scope-parser.js +120 -0
  15. package/src/security-overlay/agents/red-teamer/agent.yaml +51 -0
  16. package/src/security-overlay/agents/red-teamer/persona.md +43 -0
  17. package/src/security-overlay/data/common.txt +115 -0
  18. package/src/security-overlay/data/owasp-top10.json +15 -0
  19. package/src/security-overlay/data/tool-allowlist.json +31 -0
  20. package/src/security-overlay/skills/wize-sec-enumerate/scripts/run-enumerate.js +180 -0
  21. package/src/security-overlay/skills/wize-sec-enumerate/skill.md +32 -0
  22. package/src/security-overlay/skills/wize-sec-exploit/data/common.txt +117 -0
  23. package/src/security-overlay/skills/wize-sec-exploit/scripts/run-ffuf.js +147 -0
  24. package/src/security-overlay/skills/wize-sec-exploit/scripts/run-nikto.js +145 -0
  25. package/src/security-overlay/skills/wize-sec-exploit/scripts/run-nuclei.js +176 -0
  26. package/src/security-overlay/skills/wize-sec-exploit/scripts/run-sqlmap.js +139 -0
  27. package/src/security-overlay/skills/wize-sec-pentest/scripts/run-pipeline.js +167 -0
  28. package/src/security-overlay/skills/wize-sec-pentest/skill.md +52 -0
  29. package/src/security-overlay/skills/wize-sec-recon/scripts/run-gitleaks.js +139 -0
  30. package/src/security-overlay/skills/wize-sec-recon/scripts/run-osv.js +227 -0
  31. package/src/security-overlay/skills/wize-sec-recon/scripts/run-recon.js +162 -0
  32. package/src/security-overlay/skills/wize-sec-recon/skill.md +35 -0
  33. package/src/security-overlay/skills/wize-sec-report/scripts/render-report.js +1033 -0
  34. package/tools/installer/onboarding.js +1 -0
  35. package/tools/installer/render-shared.js +5 -1
  36. package/tools/installer/wize-cli.js +8 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ Format inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.7.0] — 2026-06-21
9
+
10
+ ### Added
11
+
12
+ - **Post-scan remediation planning (security-overlay).** Ao fim do `wize-sec-pentest`, o overlay traduz os findings em um backlog de correção pronto para `wize-create-epics-and-stories`.
13
+ - **`security-backlog.md`** gerado em `.wize/security/`: findings agrupados por tema (ex.: 97 secrets → 1 epic de rotação, não 97 stories), priorizados **P0/P1/P2** pela pior severidade do grupo, estimados S/M/L, com rastreabilidade aos findings de origem + `scope_sha256` e DoD ("re-rodar scan e confirmar finding ausente").
14
+ - Epics semeados pelo action plan do `ai-insights.json` quando presente.
15
+ - **Call-to-action** com o comando exato (`/wize-create-epics-and-stories --from .wize/security/security-backlog.md`) impresso no terminal, no `report.md` e como banner no `report.html`.
16
+ - Mantém **zero runtime próprio**: o overlay gera o backlog e imprime o comando; o usuário/agente é quem executa a skill de planejamento (o Node nunca invoca skills).
17
+
18
+ ## [0.6.0] — 2026-06-20
19
+
20
+ ### Added
21
+
22
+ - **`security-overlay` — AI Pentester (novo profile opcional).** Pipeline file-first de pentest que roda no harness do usuário (zero runtime próprio, zero dependência npm nova). Selecionável no instalador como `security-overlay`.
23
+ - **Persona `red-teamer`** + orquestradora `wize-sec-pentest` que encadeia recon → enumerate → SAST → DAST → report.
24
+ - **Gate de escopo** (`.wize/security/scope.md`, allowlist assinada com SHA-256): toda ação ofensiva é verificada por fase; alvo fora do escopo é recusado e auditado em `.refusals.log`. Default passivo; exploit ativo só com `--active`.
25
+ - **Allowlist de flags por ferramenta** (`data/tool-allowlist.json`): `--dump`/`--os-shell` e afins nunca chegam ao `execFile`, independente do input.
26
+ - **SAST**: secrets via gitleaks (com redação `***REDACTED***`) + dependências vulneráveis via osv-scanner/grype (CVE + CVSS).
27
+ - **DAST**: nuclei, nikto (safe checks), sqlmap e ffuf (content discovery), gated por `--active` quando ofensivos.
28
+ - **CVSS v3.1** zero-dep + tagger **OWASP Top 10 (2021)**.
29
+ - **Relatório** `report.md` + `report.html` self-contained (CSS inline, offline, WCAG 2.2 AA): risk score 0–100, briefing executivo, plano de ação P0/P1/P2, cobertura honesta do teste (audit confidence), recomendação por finding.
30
+ - **AI insights**: o renderer consome `ai-insights.json` escrito pelo LLM do harness (briefing + recomendações), sem chamada externa — dados ficam locais.
31
+ - **Preflight** (Epic 08): detecta SO/arch/package-manager e gera `install-pentest-tools.sh` com a fonte correta por ferramenta (apt para nmap/nikto/sqlmap; GitHub release para gitleaks/nuclei/ffuf/osv-scanner; script oficial para grype).
32
+ - Documentação completa do overlay em `.wize/planning` e `.wize/solutioning` (brief, PRD, tech-vision, NFR, architecture, 4 ADRs, 8 epics, 26+ stories).
33
+
8
34
  ## [0.5.0] — 2026-06-17
9
35
 
10
36
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "wize-dev-kit",
4
- "version": "0.5.0",
4
+ "version": "0.7.0",
5
5
  "description": "Full-lifecycle AI-assisted development kit with Test Architect and Whiteport Design Studio embedded. Inspired by BMAD Method and WDS.",
6
6
  "keywords": [
7
7
  "ai",
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ // allowlist.js — gate that filters arguments for an external pentest tool
4
+ // before they are passed to child_process.execFile. The list of allowed
5
+ // flags per tool lives in src/security-overlay/data/tool-allowlist.json.
6
+ //
7
+ // Schema (per tool, array of strings):
8
+ // "-foo" switch with no value
9
+ // "-foo:" switch that consumes the next argv as its value
10
+ // "-foo=bar" switch with a fixed value (only the literal "bar" is allowed)
11
+ // "--flag=" switch that consumes a value joined by '=' (e.g. --level=1)
12
+ //
13
+ // Invariant: args NOT in the allowlist (or args that look like values for
14
+ // flags not in the allowlist) are dropped. Positional args (targets, URLs)
15
+ // pass through unchanged.
16
+
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+
20
+ class UnknownToolError extends Error {
21
+ constructor(tool) {
22
+ super(`Unknown tool "${tool}" — not in tool-allowlist.json. Refusing to invoke.`);
23
+ this.name = 'UnknownToolError';
24
+ this.tool = tool;
25
+ }
26
+ }
27
+
28
+ const DEFAULT_ALLOWLIST_PATH = path.join(__dirname, '..', 'data', 'tool-allowlist.json');
29
+
30
+ let _cache = null;
31
+ function _loadDefault() {
32
+ if (_cache) return _cache;
33
+ const raw = fs.readFileSync(DEFAULT_ALLOWLIST_PATH, 'utf8');
34
+ _cache = JSON.parse(raw);
35
+ return _cache;
36
+ }
37
+
38
+ function loadAllowlist(filePath) {
39
+ const fp = filePath || DEFAULT_ALLOWLIST_PATH;
40
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
41
+ }
42
+
43
+ // Heuristic for "is this arg a flag?": starts with '-' and is more than 1 char
44
+ // (a bare "-" is sometimes used as stdin, treat as positional).
45
+ function isFlag(arg) {
46
+ return typeof arg === 'string' && arg.length > 1 && arg[0] === '-';
47
+ }
48
+
49
+ // Classify one allowlist token into one of:
50
+ // { kind: 'switch' } — no value
51
+ // { kind: 'colon', prefix: '-foo' } — consumes next argv (e.g. -f path)
52
+ // { kind: 'equals', prefix: '--flag=' } — consumes value joined by '=' (e.g. --level=1)
53
+ // { kind: 'literal', full: '-foo=bar' } — only the exact form is allowed
54
+ //
55
+ // When matching an arg, we try in order: literal, then colon (exact prefix
56
+ // match, e.g. arg === '-u'), then equals (arg starts with prefix, e.g.
57
+ // arg === '--level=1'), then switch (exact equality).
58
+ function classify(token) {
59
+ if (token.endsWith(':')) {
60
+ return { kind: 'colon', prefix: token.slice(0, -1) };
61
+ }
62
+ if (token.endsWith('=') && token.startsWith('--')) {
63
+ return { kind: 'equals', prefix: token };
64
+ }
65
+ if (token.includes('=') && !token.endsWith('=')) {
66
+ return { kind: 'literal', full: token };
67
+ }
68
+ return { kind: 'switch' };
69
+ }
70
+
71
+ // Given a flag arg, find the first allowlist token that matches it.
72
+ function matchFlag(toolData, arg) {
73
+ // Literal first (most specific).
74
+ for (const tok of toolData) {
75
+ if (classify(tok).kind === 'literal' && arg === tok) return { consumeNext: false };
76
+ }
77
+ // Then colon (exact prefix match) — e.g. arg === '-u'.
78
+ for (const tok of toolData) {
79
+ if (classify(tok).kind === 'colon' && arg === classify(tok).prefix) return { consumeNext: true };
80
+ }
81
+ // Then equals — e.g. arg starts with '--level='.
82
+ for (const tok of toolData) {
83
+ if (classify(tok).kind === 'equals' && arg.startsWith(classify(tok).prefix)) return { consumeNext: false };
84
+ }
85
+ // Then switch.
86
+ for (const tok of toolData) {
87
+ if (classify(tok).kind === 'switch' && tok === arg) return { consumeNext: false };
88
+ }
89
+ return null;
90
+ }
91
+
92
+ // filterArgs(tool, args, allowlist) — keep only args allowed by the tool's
93
+ // allowlist, handling value-bearing flags correctly.
94
+ function filterArgs(tool, args, allowlist) {
95
+ const data = allowlist || _loadDefault();
96
+ if (!Object.prototype.hasOwnProperty.call(data, tool)) {
97
+ throw new UnknownToolError(tool);
98
+ }
99
+ const toolData = data[tool];
100
+
101
+ const out = [];
102
+ let i = 0;
103
+ const list = args || [];
104
+ while (i < list.length) {
105
+ const arg = list[i];
106
+ if (!isFlag(arg)) {
107
+ // Positional: target, URL, output path.
108
+ out.push(arg);
109
+ i++;
110
+ continue;
111
+ }
112
+ const m = matchFlag(toolData, arg);
113
+ if (!m) {
114
+ // Unknown flag — drop the flag. We do NOT also drop the next arg,
115
+ // because a positional after a stripped flag is still a positional
116
+ // (caller may have intended both to pass). However, common patterns
117
+ // are value-bearing: e.g. `--script vuln` — if the flag is dropped,
118
+ // "vuln" is also dropped to avoid leaking. This is the safe default.
119
+ if (looksLikeValueArg(list[i + 1])) {
120
+ // Consume the next arg as part of the dropped flag's expected value.
121
+ i += 2;
122
+ } else {
123
+ i++;
124
+ }
125
+ continue;
126
+ }
127
+ out.push(arg);
128
+ if (m.consumeNext) {
129
+ // Value-bearing: the next argv is the value. Pass it through.
130
+ if (i + 1 < list.length) {
131
+ out.push(list[i + 1]);
132
+ i += 2;
133
+ } else {
134
+ i++;
135
+ }
136
+ } else {
137
+ i++;
138
+ }
139
+ }
140
+ return out;
141
+ }
142
+
143
+ // A value arg is one that does NOT start with '-' (or is the bare "-").
144
+ function looksLikeValueArg(arg) {
145
+ return arg !== undefined && (typeof arg !== 'string' || arg.length === 0 || arg[0] !== '-');
146
+ }
147
+
148
+ module.exports = {
149
+ filterArgs,
150
+ loadAllowlist,
151
+ UnknownToolError,
152
+ classify,
153
+ matchFlag
154
+ };
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ // backlog.js — turns classified security findings into a remediation
4
+ // backlog (epics + stories) that wize-create-epics-and-stories can consume.
5
+ //
6
+ // Design (per brief-post-scan):
7
+ // - Group findings by theme/section, NOT 1-story-per-finding (97 secrets
8
+ // => 1 "rotate secrets" story, not 97).
9
+ // - Priority P0/P1/P2 derived from the worst severity in the group.
10
+ // - Each story keeps traceability to the source findings + scope hash.
11
+ // - Seed epics from the AI action plan (ai-insights.json) when present.
12
+ // - Zero-dep, file-first. The overlay never invokes a skill — it prints
13
+ // a clear CTA command for the user/agent to run.
14
+
15
+ const CTA_COMMAND = '/wize-create-epics-and-stories --from .wize/security/security-backlog.md';
16
+
17
+ // Severity -> remediation priority.
18
+ function priorityFor(severity) {
19
+ switch (severity) {
20
+ case 'Critical':
21
+ case 'High':
22
+ return 'P0';
23
+ case 'Medium':
24
+ return 'P1';
25
+ default:
26
+ return 'P2'; // Low, Info-surface, None, unknown
27
+ }
28
+ }
29
+
30
+ // Group size -> rough estimate.
31
+ function estimateFor(count) {
32
+ if (count <= 2) return 'S';
33
+ if (count <= 10) return 'M';
34
+ return 'L';
35
+ }
36
+
37
+ // Human label + remediation intent per section.
38
+ const SECTION_THEME = {
39
+ secrets: { theme: 'Rotacionar e remover segredos expostos', owasp: 'A07:2021' },
40
+ deps: { theme: 'Atualizar dependências vulneráveis', owasp: 'A06:2021' },
41
+ nuclei: { theme: 'Corrigir vulnerabilidades detectadas (nuclei)', owasp: null },
42
+ nikto: { theme: 'Hardening de servidor web (nikto)', owasp: 'A05:2021' },
43
+ sqlmap: { theme: 'Corrigir injeção SQL', owasp: 'A03:2021' },
44
+ ffuf: { theme: 'Revisar endpoints/arquivos descobertos', owasp: 'A05:2021' },
45
+ tech: { theme: 'Hardening: ocultar fingerprinting de versões', owasp: 'A05:2021' },
46
+ open_ports: { theme: 'Revisar exposição de portas/serviços', owasp: 'A05:2021' },
47
+ surface: { theme: 'Revisar superfície HTTP exposta', owasp: 'A05:2021' }
48
+ };
49
+
50
+ // Severity rank for "worst in group".
51
+ const SEV_RANK = { Critical: 5, High: 4, Medium: 3, Low: 2, 'Info-surface': 1, None: 0, unknown: 0 };
52
+
53
+ // groupFindings(findings) -> [{ section, theme, owasp, count, priority,
54
+ // worstSeverity, findings: [...] }] sorted by priority then count.
55
+ function groupFindings(findings) {
56
+ const bySection = {};
57
+ for (const f of findings || []) {
58
+ const s = f.section || 'unknown';
59
+ if (!bySection[s]) bySection[s] = [];
60
+ bySection[s].push(f);
61
+ }
62
+ const groups = [];
63
+ for (const [section, fs] of Object.entries(bySection)) {
64
+ let worst = 'unknown';
65
+ for (const f of fs) {
66
+ if ((SEV_RANK[f.severity] || 0) > (SEV_RANK[worst] || 0)) worst = f.severity;
67
+ }
68
+ const theme = (SECTION_THEME[section] && SECTION_THEME[section].theme) || `Revisar findings de ${section}`;
69
+ const owasp = SECTION_THEME[section] && SECTION_THEME[section].owasp;
70
+ groups.push({
71
+ section,
72
+ theme,
73
+ owasp,
74
+ count: fs.length,
75
+ worstSeverity: worst,
76
+ priority: priorityFor(worst),
77
+ findings: fs
78
+ });
79
+ }
80
+ // Order: P0 first, then by count desc.
81
+ const pOrder = { P0: 0, P1: 1, P2: 2 };
82
+ groups.sort((a, b) => (pOrder[a.priority] - pOrder[b.priority]) || (b.count - a.count));
83
+ return groups;
84
+ }
85
+
86
+ function escapeMd(s) {
87
+ return String(s == null ? '' : s);
88
+ }
89
+
90
+ // buildBacklog({ findings, actionPlan, scopeSha, generatedAt }) -> markdown.
91
+ function buildBacklog({ findings = [], actionPlan = [], scopeSha = '', generatedAt = '' } = {}) {
92
+ const groups = groupFindings(findings);
93
+ const lines = [];
94
+
95
+ lines.push('---');
96
+ lines.push('kind: security-remediation-backlog');
97
+ lines.push('owner: red-teamer');
98
+ lines.push(`source_scope_sha256: ${escapeMd(scopeSha)}`);
99
+ lines.push(`generated_at: ${escapeMd(generatedAt)}`);
100
+ lines.push('consumed_by: wize-create-epics-and-stories');
101
+ lines.push('---');
102
+ lines.push('');
103
+ lines.push('# Security Remediation Backlog');
104
+ lines.push('');
105
+ lines.push('Backlog de correção derivado do scan de segurança. Cada epic agrupa findings por tema; cada story rastreia os findings de origem. Prioridade vem da severidade (P0 = Critical/High, P1 = Medium, P2 = Low/informativo).');
106
+ lines.push('');
107
+ lines.push(`> **Próximo passo:** rode \`${CTA_COMMAND}\` para transformar este backlog em stories formais.`);
108
+ lines.push('');
109
+
110
+ // Action plan summary (from AI insights) — the executive framing.
111
+ if (actionPlan && actionPlan.length) {
112
+ lines.push('## Plano de ação (resumo)');
113
+ lines.push('');
114
+ for (const a of actionPlan) {
115
+ lines.push(`- **[${escapeMd(a.priority)}] ${escapeMd(a.title)}** — ${escapeMd(a.detail)}`);
116
+ }
117
+ lines.push('');
118
+ }
119
+
120
+ // Build a lookup from action plan title keywords to attach detail to epics.
121
+ const planByTheme = {};
122
+ for (const a of (actionPlan || [])) planByTheme[(a.title || '').toLowerCase()] = a;
123
+
124
+ const actionable = groups.filter(g => g.worstSeverity !== 'Info-surface' || g.priority !== 'P2' ? true : true);
125
+
126
+ if (groups.length === 0) {
127
+ lines.push('## (sem itens)');
128
+ lines.push('');
129
+ lines.push('_Nenhum finding acionável neste scan — no actionable findings._');
130
+ return lines.join('\n');
131
+ }
132
+
133
+ // One epic per group.
134
+ let epicN = 0;
135
+ for (const g of groups) {
136
+ epicN++;
137
+ const est = estimateFor(g.count);
138
+ // Find a matching action-plan detail by theme keyword overlap.
139
+ let planDetail = '';
140
+ for (const [title, a] of Object.entries(planByTheme)) {
141
+ const key = g.section;
142
+ if (title.includes(key) || (key === 'secrets' && title.includes('segredo')) ||
143
+ (key === 'deps' && (title.includes('depend') || title.includes('dep'))) ||
144
+ (key === 'tech' && title.includes('fingerprint'))) {
145
+ planDetail = a.detail; break;
146
+ }
147
+ }
148
+ lines.push(`## Epic ${String(epicN).padStart(2, '0')}: ${g.theme} [${g.priority}]`);
149
+ lines.push('');
150
+ lines.push(`- **Prioridade:** ${g.priority} (pior severidade: ${g.worstSeverity})`);
151
+ lines.push(`- **Findings cobertos:** ${g.count}`);
152
+ if (g.owasp) lines.push(`- **OWASP:** ${g.owasp}`);
153
+ lines.push(`- **Estimativa:** ${est}`);
154
+ if (planDetail) lines.push(`- **Como corrigir:** ${planDetail}`);
155
+ lines.push('');
156
+ lines.push('### Stories');
157
+ lines.push('');
158
+ // For large groups, a single remediation story + a verification story.
159
+ lines.push(`- **${g.theme}** (${g.priority}, est ${est}) — corrigir os ${g.count} finding(s) de \`${g.section}\`. _Origem: ${g.section} (${g.count} findings, ${g.worstSeverity})._`);
160
+ lines.push(`- **Verificar correção de ${g.section}** (${g.priority}, est S) — re-rodar \`/wize-sec-pentest\` e confirmar que os findings de \`${g.section}\` sumiram (DoD).`);
161
+ lines.push('');
162
+ // Sample of source findings for traceability (cap at 5).
163
+ const sample = g.findings.slice(0, 5);
164
+ lines.push('<details><summary>Findings de origem (amostra)</summary>');
165
+ lines.push('');
166
+ for (const f of sample) {
167
+ lines.push(`- ${escapeMd(f.raw)}`);
168
+ }
169
+ if (g.findings.length > sample.length) {
170
+ lines.push(`- _… e mais ${g.findings.length - sample.length}._`);
171
+ }
172
+ lines.push('');
173
+ lines.push('</details>');
174
+ lines.push('');
175
+ }
176
+
177
+ return lines.join('\n');
178
+ }
179
+
180
+ module.exports = { buildBacklog, priorityFor, estimateFor, groupFindings, CTA_COMMAND };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ // cli-runner.js — small helper so each phase script can be invoked
4
+ // either as a module (import + call) or as a CLI (with --securityDir /
5
+ // --scope / --active argv). The invoke-phase helper spawns the script
6
+ // as a Node subprocess; this module bridges that to the per-phase
7
+ // `runX` function exported by each script.
8
+ //
9
+ // Usage from a script:
10
+ // const { runX } = require('./run-x');
11
+ // module.exports = { runX };
12
+ // if (require.main === module) {
13
+ // require('../../../_shared/cli-runner.js').runFromArgv({
14
+ // fn: runX,
15
+ // argMap: { securityDir: 'securityDir', scopePath: 'scopePath', active: 'active' }
16
+ // });
17
+ // }
18
+ //
19
+ // argv shape: --securityDir=PATH --scope=PATH --active [script-specific]
20
+ // The function `fn` is called with the parsed object spread.
21
+
22
+ function parseArgv(argv) {
23
+ const out = {};
24
+ for (let i = 0; i < (argv || []).length; i++) {
25
+ const a = argv[i];
26
+ if (a === '--active') { out.active = true; continue; }
27
+ const eq = a.indexOf('=');
28
+ if (eq > 0) {
29
+ const key = a.slice(0, eq).replace(/^--/, '');
30
+ const val = a.slice(eq + 1);
31
+ out[camel(key)] = val;
32
+ continue;
33
+ }
34
+ if (a.startsWith('--') && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
35
+ out[camel(a.slice(2))] = argv[i + 1];
36
+ i++;
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function camel(s) {
43
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
44
+ }
45
+
46
+ async function runFromArgv({ fn, argMap = {} }) {
47
+ const argv = process.argv.slice(2);
48
+ const parsed = parseArgv(argv);
49
+ // Map --securityDir -> securityDir, --scope -> scopePath, etc.
50
+ const opts = {};
51
+ for (const [cliKey, optKey] of Object.entries(argMap)) {
52
+ if (cliKey in parsed) opts[optKey] = parsed[cliKey];
53
+ }
54
+ // Also accept any unprefixed camelCase keys directly.
55
+ for (const [k, v] of Object.entries(parsed)) {
56
+ if (!(k in opts)) opts[k] = v;
57
+ }
58
+ if (!('active' in opts)) opts.active = false;
59
+ // The phase script may also export its own CLI flags (--target for recon).
60
+ // We forward remaining --flags as a target-agnostic extraArgs-style list.
61
+ const extras = {};
62
+ for (const a of argv) {
63
+ if (a === '--active') continue;
64
+ const m = a.match(/^--([a-z0-9-]+)/);
65
+ if (m && !argMap[m[1]] && !argMap[camel(m[1])]) {
66
+ // Stash any flag the script might want to read itself.
67
+ extras[m[1]] = a.includes('=') ? a.slice(a.indexOf('=') + 1) : true;
68
+ }
69
+ }
70
+ Object.assign(opts, extras);
71
+ try {
72
+ const r = await fn(opts);
73
+ if (r && typeof r === 'object') {
74
+ // Print a one-line summary the orchestrator can show in its summary.
75
+ const summary = r.partialStatus
76
+ ? `${process.argv[1].split('/').pop().replace(/\.js$/, '')}: partial_status=${r.partialStatus} mode=${r.mode || 'passive'}`
77
+ : '';
78
+ if (summary) process.stdout.write(summary + '\n');
79
+ }
80
+ process.exit(0);
81
+ } catch (e) {
82
+ process.stderr.write(`✖ ${process.argv[1]}: ${e && e.message ? e.message : e}\n`);
83
+ process.exit(2);
84
+ }
85
+ }
86
+
87
+ module.exports = { parseArgv, runFromArgv };
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ // cvss.js — zero-dep CVSS v3.1 score calculator. Implements the official
4
+ // formula from FIRST.org. Tested against the 5 canonical vectors from
5
+ // the spec. Returns the base score (0.0-10.0) and the severity label.
6
+
7
+ class InvalidVectorError extends Error {
8
+ constructor(message) { super(message); this.name = 'InvalidVectorError'; }
9
+ }
10
+
11
+ // --- metric value tables (per CVSS v3.1 spec) -----------------------------
12
+
13
+ const AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 };
14
+ const AC = { L: 0.77, H: 0.44 };
15
+ const PR_U = { N: 0.85, L: 0.62, H: 0.27 }; // PR and UI share values when scope=U
16
+ const PR_C = { N: 0.85, L: 0.68, H: 0.5 }; // PR changes when scope=C
17
+ const UI_U = { N: 0.85, R: 0.62 };
18
+ const UI_C = { N: 0.85, R: 0.68 };
19
+ const CIA = { H: 0.56, L: 0.22, N: 0 };
20
+
21
+ // --- parse --------------------------------------------------------------
22
+
23
+ function parse(vector) {
24
+ if (typeof vector !== 'string') throw new InvalidVectorError('vector must be a string');
25
+ let v = vector.trim();
26
+ if (v.startsWith('CVSS:3.1/')) v = v.slice('CVSS:3.1/'.length);
27
+ if (v.startsWith('CVSS:3.0/')) throw new InvalidVectorError('CVSS v3.0 vectors are not supported');
28
+
29
+ const m = {};
30
+ for (const part of v.split('/')) {
31
+ const idx = part.indexOf(':');
32
+ if (idx < 0) throw new InvalidVectorError(`bad metric segment: ${part}`);
33
+ const key = part.slice(0, idx);
34
+ const val = part.slice(idx + 1);
35
+ if (val.length !== 1) throw new InvalidVectorError(`bad metric value: ${val}`);
36
+ m[key] = val;
37
+ }
38
+
39
+ // Validate required metrics.
40
+ for (const k of ['AV', 'AC', 'PR', 'UI', 'S', 'C', 'I', 'A']) {
41
+ if (!(k in m)) throw new InvalidVectorError(`missing metric: ${k}`);
42
+ }
43
+ // Validate values.
44
+ if (!(m.AV in AV)) throw new InvalidVectorError(`invalid AV: ${m.AV}`);
45
+ if (!(m.AC in AC)) throw new InvalidVectorError(`invalid AC: ${m.AC}`);
46
+ if (!(m.PR in { N: 1, L: 1, H: 1 })) throw new InvalidVectorError(`invalid PR: ${m.PR}`);
47
+ if (!(m.UI in { N: 1, R: 1 })) throw new InvalidVectorError(`invalid UI: ${m.UI}`);
48
+ if (!(m.S in { U: 1, C: 1 })) throw new InvalidVectorError(`invalid S: ${m.S}`);
49
+ for (const k of ['C', 'I', 'A']) {
50
+ if (!(m[k] in CIA)) throw new InvalidVectorError(`invalid ${k}: ${m[k]}`);
51
+ }
52
+ // Scope C requires PR/UI to be in the {N,L,H}/{N,R} sets.
53
+ if (m.S === 'C' && !(m.PR in { N: 1, L: 1, H: 1 })) {
54
+ throw new InvalidVectorError(`invalid PR for scope C: ${m.PR}`);
55
+ }
56
+ return m;
57
+ }
58
+
59
+ // --- formula ------------------------------------------------------------
60
+
61
+ function roundUpTo1Decimal(n) {
62
+ // CVSS rounds UP to 1 decimal place. JS banker rounding rounds half to
63
+ // even, which is wrong for CVSS — we use ceiling-to-1-decimal.
64
+ return Math.ceil(n * 10) / 10;
65
+ }
66
+
67
+ function compute(vector) {
68
+ const m = parse(vector);
69
+ const scopeChanged = m.S === 'C';
70
+ const PR = scopeChanged ? PR_C[m.PR] : PR_U[m.PR];
71
+ const UI = scopeChanged ? UI_C[m.UI] : UI_U[m.UI];
72
+
73
+ // Impact.
74
+ const issBase = 1 - ((1 - CIA[m.C]) * (1 - CIA[m.I]) * (1 - CIA[m.A]));
75
+ let impact = scopeChanged
76
+ ? 7.52 * (issBase - 0.029) - 3.25 * Math.pow(issBase - 0.02, 15)
77
+ : 6.42 * issBase;
78
+ if (impact < 0) impact = 0;
79
+ impact = roundUpTo1Decimal(impact);
80
+
81
+ // Exploitability.
82
+ const exploit = 8.22 * AV[m.AV] * AC[m.AC] * PR * UI;
83
+ const explRounded = roundUpTo1Decimal(exploit);
84
+
85
+ // Base score.
86
+ let base;
87
+ if (impact <= 0) {
88
+ base = 0;
89
+ } else if (scopeChanged) {
90
+ base = 1.08 * (impact + explRounded);
91
+ } else {
92
+ base = impact + explRounded;
93
+ }
94
+ base = roundUpTo1Decimal(base);
95
+ if (base > 10) base = 10;
96
+
97
+ return { score: base, severity: severityFromScore(base) };
98
+ }
99
+
100
+ function severityFromScore(score) {
101
+ if (score === 0) return 'None';
102
+ if (score < 4.0) return 'Low';
103
+ if (score < 7.0) return 'Medium';
104
+ if (score < 9.0) return 'High';
105
+ return 'Critical';
106
+ }
107
+
108
+ module.exports = { compute, severityFromScore, parse, InvalidVectorError };