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.
- package/CHANGELOG.md +49 -0
- package/package.json +1 -1
- package/src/core-skills/wize-customize/skill.md +114 -0
- package/src/core-skills/wize-editorial-review-prose/skill.md +92 -0
- package/src/core-skills/wize-editorial-review-structure/skill.md +97 -0
- package/src/core-skills/wize-index-docs/skill.md +117 -0
- package/src/core-skills/wize-review-edge-case-hunter/skill.md +112 -0
- package/src/method-skills/2-plan-workflows/wize-edit-prd/workflow.md +108 -0
- package/src/method-skills/3-solutioning/wize-project-context/workflow.md +118 -0
- package/src/method-skills/4-implementation/wize-checkpoint-preview/workflow.md +115 -0
- package/src/method-skills/4-implementation/wize-correct-course/workflow.md +89 -0
- package/src/method-skills/4-implementation/wize-investigate/workflow.md +121 -0
- package/src/method-skills/4-implementation/wize-sprint-planning/workflow.md +58 -71
- package/src/method-skills/4-implementation/wize-sprint-status/workflow.md +29 -82
- package/src/orchestrator-skills/wize-onboarding/workflow.md +76 -14
- 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/src/tea-skills/wize-qa-generate-e2e-tests/workflow.md +119 -0
- package/tools/installer/onboarding.js +1 -0
- package/tools/installer/render-shared.js +50 -3
- 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
|
+
};
|