wize-dev-kit 0.5.0 → 0.6.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.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/security-overlay/_shared/allowlist.js +154 -0
- package/src/security-overlay/_shared/cli-runner.js +87 -0
- package/src/security-overlay/_shared/cvss.js +108 -0
- package/src/security-overlay/_shared/detect.js +125 -0
- package/src/security-overlay/_shared/install-script.js +205 -0
- package/src/security-overlay/_shared/invoke-phase.js +86 -0
- package/src/security-overlay/_shared/owasp.js +56 -0
- package/src/security-overlay/_shared/partial.js +225 -0
- package/src/security-overlay/_shared/preflight.js +175 -0
- package/src/security-overlay/_shared/scope-gate.js +172 -0
- package/src/security-overlay/_shared/scope-parser.js +120 -0
- package/src/security-overlay/agents/red-teamer/agent.yaml +51 -0
- package/src/security-overlay/agents/red-teamer/persona.md +43 -0
- package/src/security-overlay/data/common.txt +115 -0
- package/src/security-overlay/data/owasp-top10.json +15 -0
- package/src/security-overlay/data/tool-allowlist.json +31 -0
- package/src/security-overlay/skills/wize-sec-enumerate/scripts/run-enumerate.js +180 -0
- package/src/security-overlay/skills/wize-sec-enumerate/skill.md +32 -0
- package/src/security-overlay/skills/wize-sec-exploit/data/common.txt +117 -0
- package/src/security-overlay/skills/wize-sec-exploit/scripts/run-ffuf.js +147 -0
- package/src/security-overlay/skills/wize-sec-exploit/scripts/run-nikto.js +145 -0
- package/src/security-overlay/skills/wize-sec-exploit/scripts/run-nuclei.js +176 -0
- package/src/security-overlay/skills/wize-sec-exploit/scripts/run-sqlmap.js +139 -0
- package/src/security-overlay/skills/wize-sec-pentest/scripts/run-pipeline.js +157 -0
- package/src/security-overlay/skills/wize-sec-pentest/skill.md +52 -0
- package/src/security-overlay/skills/wize-sec-recon/scripts/run-gitleaks.js +139 -0
- package/src/security-overlay/skills/wize-sec-recon/scripts/run-osv.js +227 -0
- package/src/security-overlay/skills/wize-sec-recon/scripts/run-recon.js +162 -0
- package/src/security-overlay/skills/wize-sec-recon/skill.md +35 -0
- package/src/security-overlay/skills/wize-sec-report/scripts/render-report.js +999 -0
- package/tools/installer/onboarding.js +1 -0
- package/tools/installer/render-shared.js +5 -1
- package/tools/installer/wize-cli.js +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ Format inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.6.0] — 2026-06-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`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`.
|
|
13
|
+
- **Persona `red-teamer`** + orquestradora `wize-sec-pentest` que encadeia recon → enumerate → SAST → DAST → report.
|
|
14
|
+
- **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`.
|
|
15
|
+
- **Allowlist de flags por ferramenta** (`data/tool-allowlist.json`): `--dump`/`--os-shell` e afins nunca chegam ao `execFile`, independente do input.
|
|
16
|
+
- **SAST**: secrets via gitleaks (com redação `***REDACTED***`) + dependências vulneráveis via osv-scanner/grype (CVE + CVSS).
|
|
17
|
+
- **DAST**: nuclei, nikto (safe checks), sqlmap e ffuf (content discovery), gated por `--active` quando ofensivos.
|
|
18
|
+
- **CVSS v3.1** zero-dep + tagger **OWASP Top 10 (2021)**.
|
|
19
|
+
- **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.
|
|
20
|
+
- **AI insights**: o renderer consome `ai-insights.json` escrito pelo LLM do harness (briefing + recomendações), sem chamada externa — dados ficam locais.
|
|
21
|
+
- **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).
|
|
22
|
+
- Documentação completa do overlay em `.wize/planning` e `.wize/solutioning` (brief, PRD, tech-vision, NFR, architecture, 4 ADRs, 8 epics, 26+ stories).
|
|
23
|
+
|
|
8
24
|
## [0.5.0] — 2026-06-17
|
|
9
25
|
|
|
10
26
|
### 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.
|
|
4
|
+
"version": "0.6.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,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 };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// detect.js — cache-aware detector for external pentest tools.
|
|
4
|
+
//
|
|
5
|
+
// detectTools(names, { cacheDir }) -> { name: { present, path?, version? } }
|
|
6
|
+
// - present = false when the tool is not on PATH (the common case for users
|
|
7
|
+
// who haven't installed the toolchain yet). detectTools NEVER throws on
|
|
8
|
+
// a missing tool — the calling skill is expected to degrade gracefully.
|
|
9
|
+
// - path = full path to the binary (when present).
|
|
10
|
+
// - version = best-effort version string from `<bin> --version`; null if
|
|
11
|
+
// the version probe failed (timeout, non-zero exit, no output).
|
|
12
|
+
//
|
|
13
|
+
// The result is cached at <cacheDir>/.tools.json so a long pipeline
|
|
14
|
+
// (recon -> enumerate -> exploit -> report) calls `command -v` at most
|
|
15
|
+
// once per tool per session. The overlay is single-threaded per
|
|
16
|
+
// invocation, so we don't lock the cache file.
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
21
|
+
|
|
22
|
+
const CACHE_FILENAME = '.tools.json';
|
|
23
|
+
const VERSION_TIMEOUT_MS = 2000;
|
|
24
|
+
|
|
25
|
+
function cachePath(cacheDir) {
|
|
26
|
+
return path.join(cacheDir || path.join(process.cwd(), '.wize', 'security'), CACHE_FILENAME);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readCache(cacheDir) {
|
|
30
|
+
const p = cachePath(cacheDir);
|
|
31
|
+
if (!fs.existsSync(p)) return {};
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
34
|
+
} catch (_) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeCache(cacheDir, data) {
|
|
40
|
+
const dir = cacheDir || path.join(process.cwd(), '.wize', 'security');
|
|
41
|
+
try {
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
fs.writeFileSync(cachePath(cacheDir), JSON.stringify(data, null, 2), 'utf8');
|
|
44
|
+
} catch (_) {
|
|
45
|
+
// best-effort; a write failure does not invalidate the in-memory result
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function which(name) {
|
|
50
|
+
// Use `command -v` (POSIX) — returns 0 and prints the path if found.
|
|
51
|
+
// Falls back to `which` for systems without `command` (Windows cmd).
|
|
52
|
+
try {
|
|
53
|
+
const r = spawnSync('command', ['-v', name], { encoding: 'utf8', timeout: 1000 });
|
|
54
|
+
if (r.status === 0 && r.stdout) {
|
|
55
|
+
return r.stdout.split('\n')[0].trim();
|
|
56
|
+
}
|
|
57
|
+
} catch (_) { /* fall through */ }
|
|
58
|
+
try {
|
|
59
|
+
const r = spawnSync('which', [name], { encoding: 'utf8', timeout: 1000 });
|
|
60
|
+
if (r.status === 0 && r.stdout) return r.stdout.split('\n')[0].trim();
|
|
61
|
+
} catch (_) { /* fall through */ }
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function probeVersion(binPath) {
|
|
66
|
+
// Try the most common version flags in order. Each is a separate probe;
|
|
67
|
+
// a failure is non-fatal (we return null).
|
|
68
|
+
const flags = ['--version', '-version', '-V', 'version'];
|
|
69
|
+
for (const f of flags) {
|
|
70
|
+
try {
|
|
71
|
+
const out = execFileSync(binPath, [f], {
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
timeout: VERSION_TIMEOUT_MS,
|
|
74
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
75
|
+
});
|
|
76
|
+
const first = (out || '').split('\n')[0].trim();
|
|
77
|
+
if (first) return first.slice(0, 120);
|
|
78
|
+
} catch (_) {
|
|
79
|
+
// try next flag
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function probeOne(name) {
|
|
86
|
+
const p = which(name);
|
|
87
|
+
if (!p) return { present: false };
|
|
88
|
+
return { present: true, path: p, version: probeVersion(p) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// detectTools(names, opts?) -> { name: {present, path?, version?} }
|
|
92
|
+
// opts.cacheDir: where to read/write the cache file (default .wize/security).
|
|
93
|
+
function detectTools(names, opts = {}) {
|
|
94
|
+
const cacheDir = opts.cacheDir;
|
|
95
|
+
const cache = readCache(cacheDir);
|
|
96
|
+
const out = {};
|
|
97
|
+
let mutated = false;
|
|
98
|
+
|
|
99
|
+
for (const name of names || []) {
|
|
100
|
+
if (cache[name]) {
|
|
101
|
+
out[name] = cache[name];
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const entry = probeOne(name);
|
|
105
|
+
out[name] = entry;
|
|
106
|
+
cache[name] = entry;
|
|
107
|
+
mutated = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (mutated) writeCache(cacheDir, cache);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clearToolCache(opts = {}) {
|
|
115
|
+
const p = cachePath(opts.cacheDir);
|
|
116
|
+
try {
|
|
117
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
118
|
+
} catch (_) { /* ignore */ }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
detectTools,
|
|
123
|
+
clearToolCache,
|
|
124
|
+
CACHE_FILENAME
|
|
125
|
+
};
|