wize-dev-kit 0.4.1 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/package.json +1 -1
  3. package/src/core-skills/wize-customize/skill.md +114 -0
  4. package/src/core-skills/wize-editorial-review-prose/skill.md +92 -0
  5. package/src/core-skills/wize-editorial-review-structure/skill.md +97 -0
  6. package/src/core-skills/wize-index-docs/skill.md +117 -0
  7. package/src/core-skills/wize-review-edge-case-hunter/skill.md +112 -0
  8. package/src/method-skills/2-plan-workflows/wize-edit-prd/workflow.md +108 -0
  9. package/src/method-skills/3-solutioning/wize-project-context/workflow.md +118 -0
  10. package/src/method-skills/4-implementation/wize-checkpoint-preview/workflow.md +115 -0
  11. package/src/method-skills/4-implementation/wize-correct-course/workflow.md +89 -0
  12. package/src/method-skills/4-implementation/wize-investigate/workflow.md +121 -0
  13. package/src/method-skills/4-implementation/wize-sprint-planning/workflow.md +58 -71
  14. package/src/method-skills/4-implementation/wize-sprint-status/workflow.md +29 -82
  15. package/src/orchestrator-skills/wize-onboarding/workflow.md +76 -14
  16. package/src/security-overlay/_shared/allowlist.js +154 -0
  17. package/src/security-overlay/_shared/cli-runner.js +87 -0
  18. package/src/security-overlay/_shared/cvss.js +108 -0
  19. package/src/security-overlay/_shared/detect.js +125 -0
  20. package/src/security-overlay/_shared/install-script.js +205 -0
  21. package/src/security-overlay/_shared/invoke-phase.js +86 -0
  22. package/src/security-overlay/_shared/owasp.js +56 -0
  23. package/src/security-overlay/_shared/partial.js +225 -0
  24. package/src/security-overlay/_shared/preflight.js +175 -0
  25. package/src/security-overlay/_shared/scope-gate.js +172 -0
  26. package/src/security-overlay/_shared/scope-parser.js +120 -0
  27. package/src/security-overlay/agents/red-teamer/agent.yaml +51 -0
  28. package/src/security-overlay/agents/red-teamer/persona.md +43 -0
  29. package/src/security-overlay/data/common.txt +115 -0
  30. package/src/security-overlay/data/owasp-top10.json +15 -0
  31. package/src/security-overlay/data/tool-allowlist.json +31 -0
  32. package/src/security-overlay/skills/wize-sec-enumerate/scripts/run-enumerate.js +180 -0
  33. package/src/security-overlay/skills/wize-sec-enumerate/skill.md +32 -0
  34. package/src/security-overlay/skills/wize-sec-exploit/data/common.txt +117 -0
  35. package/src/security-overlay/skills/wize-sec-exploit/scripts/run-ffuf.js +147 -0
  36. package/src/security-overlay/skills/wize-sec-exploit/scripts/run-nikto.js +145 -0
  37. package/src/security-overlay/skills/wize-sec-exploit/scripts/run-nuclei.js +176 -0
  38. package/src/security-overlay/skills/wize-sec-exploit/scripts/run-sqlmap.js +139 -0
  39. package/src/security-overlay/skills/wize-sec-pentest/scripts/run-pipeline.js +157 -0
  40. package/src/security-overlay/skills/wize-sec-pentest/skill.md +52 -0
  41. package/src/security-overlay/skills/wize-sec-recon/scripts/run-gitleaks.js +139 -0
  42. package/src/security-overlay/skills/wize-sec-recon/scripts/run-osv.js +227 -0
  43. package/src/security-overlay/skills/wize-sec-recon/scripts/run-recon.js +162 -0
  44. package/src/security-overlay/skills/wize-sec-recon/skill.md +35 -0
  45. package/src/security-overlay/skills/wize-sec-report/scripts/render-report.js +999 -0
  46. package/src/tea-skills/wize-qa-generate-e2e-tests/workflow.md +119 -0
  47. package/tools/installer/onboarding.js +1 -0
  48. package/tools/installer/render-shared.js +50 -3
  49. package/tools/installer/wize-cli.js +72 -5
@@ -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
+ };
@@ -0,0 +1,205 @@
1
+ 'use strict';
2
+
3
+ // install-script.js — generates a self-contained install script for the
4
+ // security-overlay pentest tools, using the CORRECT install source per
5
+ // tool. Most security tools are NOT in apt/brew: they are Go binaries
6
+ // shipped as GitHub releases. Treating everything as a PM package
7
+ // produces a script that fails (apt has no `gitleaks`/`nuclei`/`ffuf`/
8
+ // `osv-scanner`/`grype`). So each tool declares its real method:
9
+ //
10
+ // - 'pm' : available in the OS package manager (nmap, nikto, sqlmap, curl)
11
+ // - 'github' : a GitHub release asset (gitleaks, nuclei, ffuf, osv-scanner)
12
+ // - 'script' : an official installer script (grype)
13
+ //
14
+ // The generated bash script is idempotent and installs only what's missing.
15
+
16
+ const HEADER = `#!/usr/bin/env bash
17
+ # security-overlay — install pentest tools
18
+ # Generated by wize-sec-preflight. Safe to re-run (idempotent).
19
+ #
20
+ # Most of these tools are NOT in apt/brew — they are GitHub release
21
+ # binaries. This script installs each from its correct source.
22
+ # GitHub binaries go to ~/.local/bin (no sudo needed for those).
23
+ set -euo pipefail
24
+
25
+ LOCAL_BIN="\${HOME}/.local/bin"
26
+ mkdir -p "\${LOCAL_BIN}"
27
+ case ":\${PATH}:" in
28
+ *":\${LOCAL_BIN}:"*) : ;;
29
+ *) echo "NOTE: add \${LOCAL_BIN} to your PATH (e.g. in ~/.bashrc):"
30
+ echo ' export PATH="$HOME/.local/bin:$PATH"' ;;
31
+ esac
32
+
33
+ # Detect arch for GitHub release downloads.
34
+ ARCH="$(uname -m)"
35
+ case "\${ARCH}" in
36
+ x86_64|amd64) GH_ARCH="amd64"; GH_ARCH_ALT="x64" ;;
37
+ aarch64|arm64) GH_ARCH="arm64"; GH_ARCH_ALT="arm64" ;;
38
+ *) GH_ARCH="amd64"; GH_ARCH_ALT="x64" ;;
39
+ esac
40
+ OS_LC="$(uname -s | tr '[:upper:]' '[:lower:]')"
41
+
42
+ # Helper: download a tar.gz GitHub release and install one binary from it.
43
+ gh_targz() { # $1=url $2=binary-name-inside-archive
44
+ local url="$1" bin="$2" tmp
45
+ tmp="$(mktemp -d)"
46
+ echo " ↓ \${bin} <- \${url}"
47
+ curl -fsSL "\${url}" -o "\${tmp}/a.tgz"
48
+ tar -xzf "\${tmp}/a.tgz" -C "\${tmp}"
49
+ install -m 0755 "\${tmp}/\${bin}" "\${LOCAL_BIN}/\${bin}"
50
+ rm -rf "\${tmp}"
51
+ }
52
+
53
+ gh_zip() { # $1=url $2=binary-name-inside-archive
54
+ local url="$1" bin="$2" tmp
55
+ tmp="$(mktemp -d)"
56
+ echo " ↓ \${bin} <- \${url}"
57
+ curl -fsSL "\${url}" -o "\${tmp}/a.zip"
58
+ unzip -q -o "\${tmp}/a.zip" -d "\${tmp}"
59
+ install -m 0755 "\${tmp}/\${bin}" "\${LOCAL_BIN}/\${bin}"
60
+ rm -rf "\${tmp}"
61
+ }
62
+ `;
63
+
64
+ // Per-tool install recipe. Versions are pinned to known-good releases; the
65
+ // user can bump them. {GH_ARCH}/{GH_ARCH_ALT}/{OS} are substituted by the
66
+ // bash script's own variables at run time (we emit them as shell vars).
67
+ const TOOL_RECIPES = {
68
+ // --- package-manager tools ---
69
+ nmap: { via: 'pm' },
70
+ nikto: { via: 'pm' },
71
+ sqlmap: { via: 'pm' },
72
+ curl: { via: 'pm' },
73
+
74
+ // --- GitHub release binaries ---
75
+ gitleaks: {
76
+ via: 'github',
77
+ // gitleaks_8.18.4_linux_x64.tar.gz (note: x64/arm64, not amd64)
78
+ cmd: 'gh_targz "https://github.com/gitleaks/gitleaks/releases/download/v8.18.4/gitleaks_8.18.4_${OS_LC}_${GH_ARCH_ALT}.tar.gz" gitleaks'
79
+ },
80
+ nuclei: {
81
+ via: 'github',
82
+ // nuclei_3.3.7_linux_amd64.zip
83
+ cmd: 'gh_zip "https://github.com/projectdiscovery/nuclei/releases/download/v3.3.7/nuclei_3.3.7_${OS_LC}_${GH_ARCH}.zip" nuclei'
84
+ },
85
+ ffuf: {
86
+ via: 'github',
87
+ // ffuf_2.1.0_linux_amd64.tar.gz
88
+ cmd: 'gh_targz "https://github.com/ffuf/ffuf/releases/download/v2.1.0/ffuf_2.1.0_${OS_LC}_${GH_ARCH}.tar.gz" ffuf'
89
+ },
90
+ 'osv-scanner': {
91
+ via: 'github',
92
+ // osv-scanner_linux_amd64 (raw binary, not archived)
93
+ cmd: 'echo " ↓ osv-scanner"; curl -fsSL "https://github.com/google/osv-scanner/releases/download/v2.0.2/osv-scanner_${OS_LC}_${GH_ARCH}" -o "${LOCAL_BIN}/osv-scanner"; chmod 0755 "${LOCAL_BIN}/osv-scanner"'
94
+ },
95
+
96
+ // --- official installer script ---
97
+ grype: {
98
+ via: 'script',
99
+ cmd: 'curl -fsSL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b "${LOCAL_BIN}"'
100
+ }
101
+ };
102
+
103
+ const TOOL_URLS = {
104
+ nmap: 'https://nmap.org/download.html',
105
+ nuclei: 'https://github.com/projectdiscovery/nuclei',
106
+ gitleaks: 'https://github.com/gitleaks/gitleaks',
107
+ 'osv-scanner': 'https://github.com/google/osv-scanner',
108
+ grype: 'https://github.com/anchore/grype',
109
+ nikto: 'https://github.com/sullo/nikto',
110
+ sqlmap: 'https://github.com/sqlmapproject/sqlmap',
111
+ ffuf: 'https://github.com/ffuf/ffuf'
112
+ };
113
+
114
+ const PM_INSTALL = {
115
+ apt: (pkgs) => `sudo apt-get update && sudo apt-get install -y ${pkgs.join(' ')}`,
116
+ dnf: (pkgs) => `sudo dnf install -y ${pkgs.join(' ')}`,
117
+ pacman: (pkgs) => `sudo pacman -Syu --noconfirm ${pkgs.join(' ')}`,
118
+ zypper: (pkgs) => `sudo zypper install -y ${pkgs.join(' ')}`,
119
+ apk: (pkgs) => `sudo apk add ${pkgs.join(' ')}`,
120
+ brew: (pkgs) => `brew install ${pkgs.join(' ')}`,
121
+ scoop: (pkgs) => `scoop install ${pkgs.join(' ')}`,
122
+ chocolatey: (pkgs) => `choco install -y ${pkgs.join(' ')}`
123
+ };
124
+
125
+ function generateInstallScript(preflight) {
126
+ const { os, arch, packageManager, missing = [] } = preflight;
127
+ const lines = [HEADER];
128
+
129
+ lines.push(`# OS: ${os} (${arch})`);
130
+ lines.push(`# Package manager: ${packageManager || 'none'}`);
131
+ lines.push(`# Missing tools: ${missing.length === 0 ? '(none)' : missing.join(', ')}`);
132
+ lines.push('');
133
+
134
+ if (missing.length === 0) {
135
+ lines.push('echo "All tools are already installed. Nothing to do."');
136
+ return lines.join('\n');
137
+ }
138
+
139
+ // Bucket the missing tools by install method.
140
+ const pmTools = [];
141
+ const ghTools = [];
142
+ const scriptTools = [];
143
+ const unknownTools = [];
144
+ for (const tool of missing) {
145
+ const r = TOOL_RECIPES[tool];
146
+ if (!r) { unknownTools.push(tool); continue; }
147
+ if (r.via === 'pm') pmTools.push(tool);
148
+ else if (r.via === 'github') ghTools.push(tool);
149
+ else if (r.via === 'script') scriptTools.push(tool);
150
+ }
151
+
152
+ // 1. Package-manager tools (need a known PM).
153
+ if (pmTools.length) {
154
+ const pmFn = PM_INSTALL[packageManager];
155
+ if (pmFn) {
156
+ lines.push(`echo "==> Installing via ${packageManager}: ${pmTools.join(', ')}"`);
157
+ lines.push(pmFn(pmTools));
158
+ } else {
159
+ lines.push(`echo "==> These need a package manager (none detected): ${pmTools.join(', ')}"`);
160
+ for (const t of pmTools) lines.push(`echo " ${t}: ${TOOL_URLS[t] || ''}"`);
161
+ }
162
+ lines.push('');
163
+ }
164
+
165
+ // 2. GitHub release binaries.
166
+ if (ghTools.length) {
167
+ lines.push(`echo "==> Installing GitHub-release binaries to \${LOCAL_BIN}: ${ghTools.join(', ')}"`);
168
+ for (const t of ghTools) {
169
+ lines.push(TOOL_RECIPES[t].cmd);
170
+ }
171
+ lines.push('');
172
+ }
173
+
174
+ // 3. Official installer scripts.
175
+ if (scriptTools.length) {
176
+ lines.push(`echo "==> Installing via official scripts: ${scriptTools.join(', ')}"`);
177
+ for (const t of scriptTools) {
178
+ lines.push(TOOL_RECIPES[t].cmd);
179
+ }
180
+ lines.push('');
181
+ }
182
+
183
+ // 4. Anything we don't have a recipe for.
184
+ if (unknownTools.length) {
185
+ lines.push(`echo "==> Manual install required for: ${unknownTools.join(', ')}"`);
186
+ for (const t of unknownTools) {
187
+ lines.push(`echo " ${t}: ${TOOL_URLS[t] || 'https://www.google.com/search?q=' + encodeURIComponent(t + ' install')}"`);
188
+ }
189
+ lines.push('');
190
+ }
191
+
192
+ // Verification.
193
+ lines.push('echo ""');
194
+ lines.push('echo "==> Verifying..."');
195
+ for (const tool of missing) {
196
+ lines.push(`command -v ${tool} >/dev/null 2>&1 && echo " ✓ ${tool}" || echo " ✗ ${tool} (not on PATH yet)"`);
197
+ }
198
+ lines.push('');
199
+ lines.push('echo "Done. If any tool shows ✗, ensure ~/.local/bin is on your PATH and re-open the shell."');
200
+ lines.push('echo "Then run /wize-sec-pentest."');
201
+
202
+ return lines.join('\n');
203
+ }
204
+
205
+ module.exports = { generateInstallScript, TOOL_RECIPES, TOOL_URLS, PM_INSTALL };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ // invoke-phase.js — single point that spawns a phase skill as a Node
4
+ // subprocess. The orchestrator (wize-sec-pentest) calls invokePhase to
5
+ // run each phase in sequence. Failures return {ok:false, code} — the
6
+ // orchestrator decides whether to continue or abort.
7
+ //
8
+ // Security invariants (enforced by the canary test in
9
+ // test/security-overlay/invoke-phase.test.js):
10
+ // - NEVER uses shell:true (no command-injection escape).
11
+ // - Skill names cannot escape the kit root via path traversal.
12
+
13
+ const { spawn } = require('node:child_process');
14
+ const path = require('node:path');
15
+ const fs = require('node:fs');
16
+
17
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes per phase
18
+
19
+ // resolvePhaseScript(skill, { kitRoot }) -> absolute path.
20
+ // Convention: src/security-overlay/skills/<skill>/scripts/<last-segment>.js
21
+ // E.g. wize-sec-recon -> .../skills/wize-sec-recon/scripts/recon.js.
22
+ // The path is computed even if the file does not exist (callers that want
23
+ // to verify existence should call fs.existsSync on the result).
24
+ function resolvePhaseScript(skill, opts = {}) {
25
+ if (typeof skill !== 'string' || !skill) return null;
26
+ // Reject anything that tries to escape via ../
27
+ if (skill.includes('..') || skill.includes('/') || skill.includes('\\') || path.isAbsolute(skill)) {
28
+ throw new Error(`phase-skill-name-traversal: refused ${JSON.stringify(skill)}`);
29
+ }
30
+ const kitRoot = opts.kitRoot || path.resolve(__dirname, '..', '..', '..');
31
+ // Convention: src/security-overlay/skills/<skill>/scripts/<scriptName>
32
+ // where scriptName is the explicit opts.scriptName (must include .js)
33
+ // if given, otherwise 'run-<lastSegment>.js'.
34
+ const scriptName = opts.scriptName || `run-${skill.split('-').pop()}.js`;
35
+ return path.join(kitRoot, 'src', 'security-overlay', 'skills', skill, 'scripts', scriptName);
36
+ }
37
+
38
+ // invokePhase(skill, opts) -> Promise<{ok, code, stdout, stderr, error?}>.
39
+ // opts: { kitRoot?, active?, extraArgs?: string[], timeout?: ms }
40
+ // Does NOT throw on subprocess failure — returns {ok:false, code, error}.
41
+ function invokePhase(skill, opts = {}) {
42
+ return new Promise(resolve => {
43
+ let script;
44
+ try {
45
+ script = resolvePhaseScript(skill, opts);
46
+ } catch (e) {
47
+ return resolve({ ok: false, code: -1, stdout: '', stderr: '', error: String(e.message || e) });
48
+ }
49
+ if (!script || !fs.existsSync(script)) {
50
+ return resolve({ ok: false, code: -1, stdout: '', stderr: '', error: `phase script not found for ${skill}` });
51
+ }
52
+
53
+ const argv = [];
54
+ if (opts.active) argv.push('--active');
55
+ if (opts.securityDir) argv.push(`--securityDir=${opts.securityDir}`);
56
+ if (opts.scopePath) argv.push(`--scope=${opts.scopePath}`);
57
+ if (Array.isArray(opts.extraArgs)) argv.push(...opts.extraArgs);
58
+
59
+ const child = spawn(process.execPath, [script, ...argv], {
60
+ stdio: ['ignore', 'pipe', 'pipe'],
61
+ // shell: false is the default in spawn(); we do not set it.
62
+ timeout: opts.timeout || DEFAULT_TIMEOUT_MS
63
+ });
64
+
65
+ let stdout = '';
66
+ let stderr = '';
67
+ child.stdout.on('data', d => { stdout += d.toString(); });
68
+ child.stderr.on('data', d => { stderr += d.toString(); });
69
+
70
+ child.on('error', err => {
71
+ resolve({ ok: false, code: -1, stdout, stderr, error: String(err.message || err) });
72
+ });
73
+ child.on('close', code => {
74
+ resolve({ ok: code === 0, code: code == null ? -1 : code, stdout, stderr });
75
+ });
76
+ });
77
+ }
78
+
79
+ // Exposed for tests that need to introspect the spawn call.
80
+ function _spawnForTest(...args) { return spawn(...args); }
81
+
82
+ module.exports = {
83
+ invokePhase,
84
+ resolvePhaseScript,
85
+ _spawnForTest
86
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ // owasp.js — maps a finding (rule id / cve / poc) to an OWASP Top 10 (2021)
4
+ // category. Zero-dep: the category table is loaded from
5
+ // src/security-overlay/data/owasp-top10.json.
6
+
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+
10
+ const DATA_PATH = path.join(__dirname, '..', 'data', 'owasp-top10.json');
11
+
12
+ let _cache = null;
13
+ function _load() {
14
+ if (_cache) return _cache;
15
+ const raw = fs.readFileSync(DATA_PATH, 'utf8');
16
+ const parsed = JSON.parse(raw);
17
+ _cache = parsed.categories || [];
18
+ return _cache;
19
+ }
20
+
21
+ function listOwaspCategories() {
22
+ return _load().slice();
23
+ }
24
+
25
+ function listOwaspCategoryIds() {
26
+ return _load().map(c => c.id);
27
+ }
28
+
29
+ // Ordered rules: the FIRST match wins. We check cve before generic rule
30
+ // because a finding with a CVE is unambiguously A06 (vulnerable components).
31
+ const RULES = [
32
+ { match: f => !!f.cve, to: 'A06:2021' },
33
+ { match: f => /sqli|sql[-_ ]?injection|injection/i.test(f.rule || ''), to: 'A03:2021' },
34
+ { match: f => /\bxss\b/i.test(f.rule || ''), to: 'A03:2021' },
35
+ { match: f => /\bauth[-_ ]?bypass|auth[-_ ]?bypass|session/i.test(f.rule || ''), to: 'A07:2021' },
36
+ { match: f => /\btls|cert|cipher|\bssl\b/i.test(f.rule || ''), to: 'A02:2021' },
37
+ { match: f => /\bcors|\bcsp|headers?|\bhttponly/i.test(f.rule || ''), to: 'A05:2021' },
38
+ { match: f => /\bexposed|\bdisclosed|server-status|admin\b/i.test(f.rule || ''), to: 'A05:2021' },
39
+ { match: f => /\bssrf|redirect/i.test(f.rule || ''), to: 'A10:2021' }
40
+ ];
41
+
42
+ function tagOwasp(finding) {
43
+ if (!finding || typeof finding !== 'object') return 'UNKNOWN';
44
+ for (const r of RULES) {
45
+ try {
46
+ if (r.match(finding)) return r.to;
47
+ } catch (_) { /* defensive: never throw from a tagger */ }
48
+ }
49
+ return 'UNKNOWN';
50
+ }
51
+
52
+ module.exports = {
53
+ tagOwasp,
54
+ listOwaspCategories,
55
+ listOwaspCategoryIds
56
+ };