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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// preflight.js — detects the host environment (OS/arch/package manager)
|
|
4
|
+
// and which tools from data/tool-allowlist.json are installed.
|
|
5
|
+
//
|
|
6
|
+
// Test hook: when WIZE_SEC_PREFLIGHT_OS, WIZE_SEC_PREFLIGHT_PM, and
|
|
7
|
+
// WIZE_SEC_PREFLIGHT_TOOLS (JSON) env vars are set, the real probes
|
|
8
|
+
// are skipped and the values are returned directly. This lets tests
|
|
9
|
+
// simulate Mac/Linux/Windows-WSL without actually running `which` etc.
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
15
|
+
|
|
16
|
+
const ALLOWLIST_PATH = path.join(__dirname, '..', 'data', 'tool-allowlist.json');
|
|
17
|
+
|
|
18
|
+
function readToolNames() {
|
|
19
|
+
try {
|
|
20
|
+
const data = JSON.parse(fs.readFileSync(ALLOWLIST_PATH, 'utf8'));
|
|
21
|
+
// Skip _schema and any non-array fields.
|
|
22
|
+
return Object.keys(data).filter(k => k !== '_schema' && Array.isArray(data[k]));
|
|
23
|
+
} catch (_) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function detectOS() {
|
|
29
|
+
if (process.env.WIZE_SEC_PREFLIGHT_OS) return process.env.WIZE_SEC_PREFLIGHT_OS;
|
|
30
|
+
const platform = os.platform();
|
|
31
|
+
if (platform === 'linux') {
|
|
32
|
+
// Detect WSL by reading /proc/version for "Microsoft" or "WSL".
|
|
33
|
+
try {
|
|
34
|
+
const v = fs.readFileSync('/proc/version', 'utf8');
|
|
35
|
+
if (/microsoft|wsl/i.test(v)) return 'wsl';
|
|
36
|
+
} catch (_) { /* not linux */ }
|
|
37
|
+
return 'linux';
|
|
38
|
+
}
|
|
39
|
+
if (platform === 'darwin') return 'darwin';
|
|
40
|
+
if (platform === 'win32') return 'win32';
|
|
41
|
+
return 'linux';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectArch() {
|
|
45
|
+
if (process.env.WIZE_SEC_PREFLIGHT_ARCH) return process.env.WIZE_SEC_PREFLIGHT_ARCH;
|
|
46
|
+
return process.arch; // 'x64' | 'arm64' | 'ia32' etc.
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function detectPackageManager(os) {
|
|
50
|
+
if (process.env.WIZE_SEC_PREFLIGHT_PM) return process.env.WIZE_SEC_PREFLIGHT_PM;
|
|
51
|
+
// Check for the most common PMs in order.
|
|
52
|
+
const candidates = {
|
|
53
|
+
linux: ['apt', 'dnf', 'pacman', 'zypper', 'apk'],
|
|
54
|
+
wsl: ['apt', 'dnf', 'pacman'],
|
|
55
|
+
darwin: ['brew'],
|
|
56
|
+
win32: ['scoop', 'chocolatey']
|
|
57
|
+
};
|
|
58
|
+
const list = candidates[os] || [];
|
|
59
|
+
for (const pm of list) {
|
|
60
|
+
const cmds = { apt: 'apt', dnf: 'dnf', pacman: 'pacman', zypper: 'zypper', apk: 'apk', brew: 'brew', scoop: 'scoop', chocolatey: 'choco' }[pm];
|
|
61
|
+
try {
|
|
62
|
+
execFileSync(cmds, ['--version'], { stdio: 'ignore' });
|
|
63
|
+
return pm;
|
|
64
|
+
} catch (_) { /* not present */ }
|
|
65
|
+
}
|
|
66
|
+
return 'none';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function whichCommand(os) {
|
|
70
|
+
// 'command -v' on POSIX, 'where' on Windows.
|
|
71
|
+
if (os === 'win32') return 'where';
|
|
72
|
+
return 'command';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function whichArg(os) {
|
|
76
|
+
if (os === 'win32') return [];
|
|
77
|
+
return ['-v'];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function probeWhich(name, os) {
|
|
81
|
+
try {
|
|
82
|
+
const r = spawnSync(whichCommand(os), [...whichArg(os), name], { encoding: 'utf8', timeout: 2000 });
|
|
83
|
+
if (r.status === 0 && r.stdout) {
|
|
84
|
+
return r.stdout.split('\n')[0].trim();
|
|
85
|
+
}
|
|
86
|
+
} catch (_) { /* not present */ }
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function probeVersion(binPath, os) {
|
|
91
|
+
// Try common version flags. Each is a separate probe; failure is non-fatal.
|
|
92
|
+
const flags = ['--version', '-version', '-V', 'version'];
|
|
93
|
+
for (const f of flags) {
|
|
94
|
+
try {
|
|
95
|
+
const out = execFileSync(binPath, [f], { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
96
|
+
const first = (out || '').split('\n')[0].trim();
|
|
97
|
+
if (first) return first.slice(0, 120);
|
|
98
|
+
} catch (_) { /* try next */ }
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function detectTools(os) {
|
|
104
|
+
const names = readToolNames();
|
|
105
|
+
const tools = {};
|
|
106
|
+
// Initialize all tools as missing.
|
|
107
|
+
for (const n of names) tools[n] = { present: false };
|
|
108
|
+
// Test hook: parse the JSON env var. The hook marks the listed tools as
|
|
109
|
+
// present; everything else stays missing.
|
|
110
|
+
if (process.env.WIZE_SEC_PREFLIGHT_TOOLS) {
|
|
111
|
+
try {
|
|
112
|
+
const m = JSON.parse(process.env.WIZE_SEC_PREFLIGHT_TOOLS);
|
|
113
|
+
for (const [name, path] of Object.entries(m)) {
|
|
114
|
+
if (!(name in tools)) tools[name] = { present: !!path, path: path || undefined, version: null };
|
|
115
|
+
else {
|
|
116
|
+
tools[name] = { present: !!path, path: path || undefined, version: null };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (_) { /* fall through to real detection */ }
|
|
120
|
+
return tools;
|
|
121
|
+
}
|
|
122
|
+
// Real detection.
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
const p = probeWhich(name, os);
|
|
125
|
+
if (!p) {
|
|
126
|
+
tools[name] = { present: false };
|
|
127
|
+
} else {
|
|
128
|
+
tools[name] = { present: true, path: p, version: probeVersion(p, os) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return tools;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function runPreflight(opts = {}) {
|
|
135
|
+
const os_ = detectOS();
|
|
136
|
+
const arch = detectArch();
|
|
137
|
+
const pm = detectPackageManager(os_);
|
|
138
|
+
const tools = detectTools(os_);
|
|
139
|
+
const missing = Object.entries(tools).filter(([, v]) => !v.present).map(([k]) => k);
|
|
140
|
+
return {
|
|
141
|
+
os: os_,
|
|
142
|
+
arch,
|
|
143
|
+
packageManager: pm,
|
|
144
|
+
tools,
|
|
145
|
+
missing,
|
|
146
|
+
node: process.version,
|
|
147
|
+
nodePath: process.execPath
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatReport(p) {
|
|
152
|
+
const present = Object.entries(p.tools).filter(([, v]) => v.present);
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push(`OS: ${p.os} (${p.arch})`);
|
|
155
|
+
lines.push(`Package manager: ${p.packageManager || 'none'}`);
|
|
156
|
+
lines.push(`Node: ${p.node}`);
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push(`Tools: ${present.length} present, ${p.missing.length} missing`);
|
|
159
|
+
if (present.length) {
|
|
160
|
+
lines.push(' present:');
|
|
161
|
+
for (const [name, info] of present) {
|
|
162
|
+
const v = info.version ? ` — ${info.version}` : '';
|
|
163
|
+
lines.push(` ✓ ${name}${v}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (p.missing.length) {
|
|
167
|
+
lines.push(' missing:');
|
|
168
|
+
for (const name of p.missing) lines.push(` ✗ ${name}`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
lines.push('Run the install script (see .wize/security/install-pentest-tools.sh) to add the missing tools.');
|
|
171
|
+
}
|
|
172
|
+
return lines.join('\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { runPreflight, formatReport, readToolNames };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scope-gate.js — single point that decides whether an offensive tool may run
|
|
4
|
+
// against a given target. ADR-001: this module is THE gate; skills must call
|
|
5
|
+
// assertTargetInScope before any execFile.
|
|
6
|
+
//
|
|
7
|
+
// Refusals (returns false) are logged to .wize/security/.refusals.log with
|
|
8
|
+
// ISO-8601 timestamp + target + reason. Scope validation errors (ScopeError)
|
|
9
|
+
// propagate — they signal an invalid scope, not a refused action.
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
parseScope,
|
|
16
|
+
validateScope,
|
|
17
|
+
ScopeError
|
|
18
|
+
} = require('./scope-parser.js');
|
|
19
|
+
|
|
20
|
+
const REFSUAL_LOG_FILENAME = '.refusals.log';
|
|
21
|
+
|
|
22
|
+
// --- body parsing --------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
// Parse the body of a scope.md into a structured allowlist. We accept the
|
|
25
|
+
// shape defined in ADR-002:
|
|
26
|
+
//
|
|
27
|
+
// ## allowlist
|
|
28
|
+
// hosts:
|
|
29
|
+
// - localhost
|
|
30
|
+
// - 127.0.0.1
|
|
31
|
+
// urls:
|
|
32
|
+
// - https://staging.example.internal/api/
|
|
33
|
+
// paths:
|
|
34
|
+
// - /api
|
|
35
|
+
//
|
|
36
|
+
// Zero-dep: each list block is matched line-by-line under its `## allowlist`
|
|
37
|
+
// heading until the next `##` heading or EOF. Empty / missing blocks yield
|
|
38
|
+
// empty arrays (which makes EVERY target fail the allowlist — fail-closed).
|
|
39
|
+
function parseAllowlist(body) {
|
|
40
|
+
const out = { hosts: [], urls: [], paths: [] };
|
|
41
|
+
const lines = String(body || '').split('\n');
|
|
42
|
+
let section = null;
|
|
43
|
+
let key = null;
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const h = line.match(/^##\s+([a-zA-Z_][a-zA-Z0-9_-]*)/);
|
|
46
|
+
if (h) {
|
|
47
|
+
section = h[1] === 'allowlist' ? 'allowlist' : null;
|
|
48
|
+
key = null;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (section !== 'allowlist') continue;
|
|
52
|
+
const km = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*$/);
|
|
53
|
+
if (km) {
|
|
54
|
+
const k = km[1];
|
|
55
|
+
if (k === 'hosts' || k === 'urls' || k === 'paths') key = k;
|
|
56
|
+
else key = null;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const lm = line.match(/^\s+-\s+(.+?)\s*$/);
|
|
60
|
+
if (lm && key) {
|
|
61
|
+
out[key].push(lm[1]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- matching ------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function matchHost(allowlist, host) {
|
|
70
|
+
return allowlist.hosts.some(h => h === host);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function matchUrl(allowlist, url) {
|
|
74
|
+
// Normalize by stripping trailing slashes so 'http://x/' and 'http://x'
|
|
75
|
+
// match the same prefix.
|
|
76
|
+
const norm = s => String(s || '').replace(/\/+$/, '');
|
|
77
|
+
const target = norm(url);
|
|
78
|
+
return allowlist.urls.some(prefix => target.startsWith(norm(prefix)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function matchPath(allowlist, p) {
|
|
82
|
+
// The path must equal or start with one of the allowlisted paths.
|
|
83
|
+
return allowlist.paths.some(ap => p === ap || p.startsWith(ap.endsWith('/') ? ap : ap + '/'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- public api ----------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
// loadScope(scopePath) — load + parse + validate. On error: log refusal
|
|
89
|
+
// (best-effort) and rethrow. The caller (a skill) aborts on throw.
|
|
90
|
+
function loadScope(scopePath) {
|
|
91
|
+
if (!fs.existsSync(scopePath)) {
|
|
92
|
+
// Cannot log refusal without a known scope directory; rely on caller.
|
|
93
|
+
throw new ScopeError('MISSING_FILE', null,
|
|
94
|
+
`scope.md ausente em ${scopePath} — crie e assine em .wize/security/scope.md antes de rodar o pipeline`);
|
|
95
|
+
}
|
|
96
|
+
const text = fs.readFileSync(scopePath, 'utf8');
|
|
97
|
+
const scope = parseScope(text);
|
|
98
|
+
validateScope(scope);
|
|
99
|
+
return scope;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// assertTargetInScope(scope, target, { refusalsDir }) -> boolean.
|
|
103
|
+
// On refusal, writes a line to <refusalsDir>/.refusals.log and returns false.
|
|
104
|
+
// Throws ScopeError if `scope` itself is invalid (HASH_MISMATCH, etc.) —
|
|
105
|
+
// callers should treat that as abort-the-pipeline.
|
|
106
|
+
function assertTargetInScope(scope, target, opts = {}) {
|
|
107
|
+
const refusalsDir = opts.refusalsDir || path.join(process.cwd(), '.wize', 'security');
|
|
108
|
+
|
|
109
|
+
// Validate the scope up front so any tampering is surfaced loudly. We
|
|
110
|
+
// also log the attempt before re-throwing — an invalid scope is itself
|
|
111
|
+
// a refusal event that must appear in the audit trail.
|
|
112
|
+
try {
|
|
113
|
+
validateScope(scope);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err && err.code) {
|
|
116
|
+
logRefusal(refusalsDir, target || {}, `${err.code}: ${String(err.message || '').slice(0, 200)}`);
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const allowlist = parseAllowlist(scope.body);
|
|
122
|
+
|
|
123
|
+
// Evaluate each provided dimension of the target. A dimension is "in scope"
|
|
124
|
+
// iff it matches an allowlist entry. If the caller provides multiple
|
|
125
|
+
// dimensions (e.g. {host, url}), all of them must match.
|
|
126
|
+
const checks = [];
|
|
127
|
+
if (target.host) checks.push({ ok: matchHost(allowlist, target.host), why: 'host not in allowlist' });
|
|
128
|
+
if (target.url) checks.push({ ok: matchUrl(allowlist, target.url), why: 'url not in allowlist' });
|
|
129
|
+
if (target.path) checks.push({ ok: matchPath(allowlist, target.path), why: 'path not in allowlist' });
|
|
130
|
+
|
|
131
|
+
if (checks.length === 0) {
|
|
132
|
+
// Defensive: refusing a target we can't classify is the safe default.
|
|
133
|
+
logRefusal(refusalsDir, target, 'no target dimension provided');
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const allIn = checks.every(c => c.ok);
|
|
138
|
+
if (!allIn) {
|
|
139
|
+
const reason = checks.filter(c => !c.ok).map(c => c.why).join('; ');
|
|
140
|
+
logRefusal(refusalsDir, target, reason);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// logRefusal(refusalsDir, target, reason) — append a YAML line to
|
|
147
|
+
// .wize/security/.refusals.log. Best-effort: never throws (a logging failure
|
|
148
|
+
// must not mask the refusal that caused it).
|
|
149
|
+
function logRefusal(refusalsDir, target, reason) {
|
|
150
|
+
try {
|
|
151
|
+
fs.mkdirSync(refusalsDir, { recursive: true });
|
|
152
|
+
const file = path.join(refusalsDir, REFSUAL_LOG_FILENAME);
|
|
153
|
+
const entry = [
|
|
154
|
+
'-',
|
|
155
|
+
` timestamp: ${new Date().toISOString()}`,
|
|
156
|
+
...Object.entries(target || {}).map(([k, v]) => ` ${k}: ${String(v).replace(/\n/g, ' ')}`),
|
|
157
|
+
` reason: ${String(reason || 'unspecified').replace(/\n/g, ' ')}`
|
|
158
|
+
].join('\n') + '\n';
|
|
159
|
+
fs.appendFileSync(file, entry, 'utf8');
|
|
160
|
+
} catch (_) {
|
|
161
|
+
// Intentionally swallow — refusing to crash the caller is more important
|
|
162
|
+
// than refusing to log. The gate decision (return false) is what matters.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
loadScope,
|
|
168
|
+
assertTargetInScope,
|
|
169
|
+
logRefusal,
|
|
170
|
+
parseAllowlist,
|
|
171
|
+
ScopeError
|
|
172
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scope-parser.js — file-first parser/validator for `.wize/security/scope.md`.
|
|
4
|
+
//
|
|
5
|
+
// Format (ADR-002):
|
|
6
|
+
// ---
|
|
7
|
+
// accepted_by: <string>
|
|
8
|
+
// accepted_at: <ISO-8601>
|
|
9
|
+
// scope_sha256: <hex SHA-256 of the body below>
|
|
10
|
+
// ---
|
|
11
|
+
//
|
|
12
|
+
// ## allowlist
|
|
13
|
+
// ...
|
|
14
|
+
//
|
|
15
|
+
// This module is the single source of truth for parsing + validating the
|
|
16
|
+
// scope. Skills that touch offensive tools must call loadScope() and abort
|
|
17
|
+
// on any ScopeError.
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const crypto = require('node:crypto');
|
|
21
|
+
|
|
22
|
+
class ScopeError extends Error {
|
|
23
|
+
constructor(code, field, message) {
|
|
24
|
+
super(message || `${code}${field ? ` (${field})` : ''}`);
|
|
25
|
+
this.name = 'ScopeError';
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.field = field || null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const REQUIRED_FIELDS = ['accepted_by', 'accepted_at', 'scope_sha256'];
|
|
32
|
+
|
|
33
|
+
// Split a `scope.md` text into { frontmatter, body }.
|
|
34
|
+
// The frontmatter is the YAML block delimited by `---` lines at the very top.
|
|
35
|
+
// We do NOT use a YAML library (zero-dep); we accept a flat `key: value` shape
|
|
36
|
+
// only, which is the entire scope of this overlay's frontmatter (ADR-002).
|
|
37
|
+
function parseScope(mdText) {
|
|
38
|
+
if (typeof mdText !== 'string' || !mdText.startsWith('---\n') && mdText !== '---') {
|
|
39
|
+
throw new ScopeError('INVALID_FORMAT', null,
|
|
40
|
+
'scope.md must start with a YAML frontmatter block (--- on line 1)');
|
|
41
|
+
}
|
|
42
|
+
// Normalize: accept either "---\n...\n---\n\nbody" or "---\n...\n---\nbody".
|
|
43
|
+
// The trailing \n after the closing --- is part of the separator, NOT the body.
|
|
44
|
+
// This is what the user signs when they run --sign-scope (and what they
|
|
45
|
+
// expect when the hash is computed on the body they wrote).
|
|
46
|
+
const fmMatch = mdText.match(/^---\n([\s\S]*?)\n---\n/);
|
|
47
|
+
if (!fmMatch) {
|
|
48
|
+
throw new ScopeError('INVALID_FORMAT', null,
|
|
49
|
+
'scope.md frontmatter is missing or not terminated by a second --- line');
|
|
50
|
+
}
|
|
51
|
+
const fmText = fmMatch[1];
|
|
52
|
+
const body = mdText.slice(fmMatch[0].length);
|
|
53
|
+
|
|
54
|
+
const frontmatter = {};
|
|
55
|
+
for (const line of fmText.split('\n')) {
|
|
56
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*?)\s*$/);
|
|
57
|
+
if (!m) continue;
|
|
58
|
+
frontmatter[m[1]] = m[2].replace(/^['"]|['"]$/g, ''); // strip wrapping quotes
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { frontmatter, body };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate a parsed scope. Returns true on success; throws ScopeError otherwise.
|
|
65
|
+
// opts: { now?: Date } — for future `accepted_at` warning. Currently unused but
|
|
66
|
+
// reserved so we can add a warn() channel without breaking callers.
|
|
67
|
+
function validateScope(scope, opts = {}) {
|
|
68
|
+
if (!scope || typeof scope !== 'object') {
|
|
69
|
+
throw new ScopeError('INVALID_FORMAT', null, 'scope is not an object');
|
|
70
|
+
}
|
|
71
|
+
const fm = scope.frontmatter || {};
|
|
72
|
+
for (const field of REQUIRED_FIELDS) {
|
|
73
|
+
if (!fm[field] || typeof fm[field] !== 'string' || fm[field].trim() === '') {
|
|
74
|
+
throw new ScopeError('MISSING_FIELDS', field,
|
|
75
|
+
`scope.md frontmatter is missing required field "${field}" — ` +
|
|
76
|
+
`crie e assine com "wize-sec-pentest --sign-scope" ou preencha manualmente`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Hash integrity. Compute SHA-256 of the body and compare to scope_sha256.
|
|
81
|
+
// We re-parse the body as-is (already stripped of frontmatter) — the original
|
|
82
|
+
// signing was done on the raw body bytes, so byte-equality matters.
|
|
83
|
+
const expected = computeScopeSha256(scope.body || '');
|
|
84
|
+
if (expected !== fm.scope_sha256) {
|
|
85
|
+
throw new ScopeError('HASH_MISMATCH', 'scope_sha256',
|
|
86
|
+
`scope.md body was modified after acceptance (expected ${expected.slice(0, 12)}…, ` +
|
|
87
|
+
`got ${String(fm.scope_sha256).slice(0, 12)}…) — re-assine com "wize-sec-pentest --sign-scope"`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Soft check: accepted_at in the future is a warning, not an error.
|
|
91
|
+
// We don't surface the warning through this function (no logger plumbed
|
|
92
|
+
// here); consumers can inspect frontmatter.accepted_at themselves.
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function computeScopeSha256(bodyText) {
|
|
98
|
+
return crypto.createHash('sha256').update(String(bodyText || ''), 'utf8').digest('hex');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read + parse + validate a scope.md file. The single entry point used by
|
|
102
|
+
// every offensive skill (AC-E02-1, AC-E02-3).
|
|
103
|
+
function loadScope(scopePath) {
|
|
104
|
+
if (!fs.existsSync(scopePath)) {
|
|
105
|
+
throw new ScopeError('MISSING_FILE', null,
|
|
106
|
+
`scope.md ausente em ${scopePath} — crie e assine em .wize/security/scope.md antes de rodar o pipeline`);
|
|
107
|
+
}
|
|
108
|
+
const text = fs.readFileSync(scopePath, 'utf8');
|
|
109
|
+
const scope = parseScope(text);
|
|
110
|
+
validateScope(scope);
|
|
111
|
+
return scope;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
parseScope,
|
|
116
|
+
validateScope,
|
|
117
|
+
computeScopeSha256,
|
|
118
|
+
loadScope,
|
|
119
|
+
ScopeError
|
|
120
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
code: wize-sec-red-teamer
|
|
2
|
+
name: red-teamer
|
|
3
|
+
title: Security Overlay — Red-Teamer
|
|
4
|
+
icon: "🔓"
|
|
5
|
+
team: software-development
|
|
6
|
+
module: security-overlay
|
|
7
|
+
phase: "4-implementation (per-project, gated)"
|
|
8
|
+
|
|
9
|
+
description: |
|
|
10
|
+
Red-teamer is the offensive pentester for the security-overlay. Drives
|
|
11
|
+
the recon -> enumerate -> exploit -> report pipeline against targets
|
|
12
|
+
the user has explicitly authorized in .wize/security/scope.md. Runs
|
|
13
|
+
inside the user's AI harness — never as a remote service.
|
|
14
|
+
|
|
15
|
+
style:
|
|
16
|
+
voice: "pragmatic, direct, no-flourish pentester"
|
|
17
|
+
brevity: "high — finding + impact + PoC"
|
|
18
|
+
approach: "always asks: is this in scope, and do I have --active?"
|
|
19
|
+
|
|
20
|
+
overlay: security
|
|
21
|
+
|
|
22
|
+
skills:
|
|
23
|
+
- wize-sec-pentest
|
|
24
|
+
|
|
25
|
+
commands:
|
|
26
|
+
- /wize-sec-pentest
|
|
27
|
+
|
|
28
|
+
inputs:
|
|
29
|
+
- ".wize/security/scope.md (gate of authorization)"
|
|
30
|
+
- ".wize/security/.tools.json (detection cache)"
|
|
31
|
+
- ".wize/security/.refusals.log (audit trail of refusals)"
|
|
32
|
+
|
|
33
|
+
outputs:
|
|
34
|
+
- ".wize/security/recon.md"
|
|
35
|
+
- ".wize/security/enumerate.md"
|
|
36
|
+
- ".wize/security/sast.md"
|
|
37
|
+
- ".wize/security/dast.md"
|
|
38
|
+
- ".wize/security/report.md"
|
|
39
|
+
- ".wize/security/report.html"
|
|
40
|
+
|
|
41
|
+
hand_off:
|
|
42
|
+
to_tea: |
|
|
43
|
+
Findings of severity High or Critical should be reviewable by
|
|
44
|
+
Hawkeye/TEA in the implementation gate of the security-overlay
|
|
45
|
+
itself. The red-teamer's results are inputs to the user's security
|
|
46
|
+
review, NOT a substitute for it.
|
|
47
|
+
|
|
48
|
+
non_negotiables:
|
|
49
|
+
- "Default passive — never run an offensive tool without scope + --active."
|
|
50
|
+
- "Findings of secrets list file+line; the secret VALUE never appears in report.html."
|
|
51
|
+
- "Refusals are audited to .wize/security/.refusals.log (no silent failure)."
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# red-teamer — Security Overlay Persona
|
|
2
|
+
|
|
3
|
+
## Identity
|
|
4
|
+
|
|
5
|
+
I am **red-teamer**. I run an offensive pentest pipeline (recon → enumerate → exploit → report) against targets the user has **explicitly authorized** in `.wize/security/scope.md`. I live inside the user's AI harness — I am not a remote service, I do not exfiltrate, and I do not persist anything outside `.wize/security/`.
|
|
6
|
+
|
|
7
|
+
I am a pentester who respects the escopo. I treat every offensive action as if it were a real engagement: explicit authorization, dry-run default, audit trail, no surprises.
|
|
8
|
+
|
|
9
|
+
## What I do
|
|
10
|
+
|
|
11
|
+
| Phase | Tooling | Output |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **recon** | nmap | `recon.md` (ports, services) |
|
|
14
|
+
| **enumerate** | nuclei (passive), curl probing | `enumerate.md` (endpoints, tech) |
|
|
15
|
+
| **sast** | gitleaks (secrets), osv-scanner / grype (deps) | `sast.md` (findings) |
|
|
16
|
+
| **exploit (DAST)** | nuclei, nikto, sqlmap, ffuf | `dast.md` (findings + PoC) |
|
|
17
|
+
| **report** | local render (MD + HTML self-contained) | `report.md`, `report.html` |
|
|
18
|
+
|
|
19
|
+
Each phase is a standalone skill; the orchestrator `wize-sec-pentest` chains them.
|
|
20
|
+
|
|
21
|
+
## How I work
|
|
22
|
+
|
|
23
|
+
- **Default passivo.** Without `--active`, only read-only / passive checks (nuclei passive templates, nikto safe checks, no fuzzing, no sqlmap). Active exploitation requires the explicit flag.
|
|
24
|
+
- **Scope is the gate.** Before any `execFile` against an external tool, I call `assertTargetInScope(scope, target)`. If the target is not in the allowlist, the action is refused and logged to `.wize/security/.refusals.log`. No exceptions.
|
|
25
|
+
- **Ferramentas ausentes degradam, não abortam.** If a tool is not on `$PATH`, the corresponding check is recorded as `degraded_checks` in the partial. The pipeline continues.
|
|
26
|
+
- **Flags via allowlist.** Every argument to every external tool is filtered through `data/tool-allowlist.json`. I never pass user-supplied flags directly to `execFile`.
|
|
27
|
+
|
|
28
|
+
## Limits
|
|
29
|
+
|
|
30
|
+
- I do NOT attack hosts, URLs, or paths outside the `scope.md` allowlist. Even with `--active`.
|
|
31
|
+
- I do NOT log or persist secrets (their values are redacted to `***REDACTED***` in the HTML report; the partial keeps file+line only).
|
|
32
|
+
- I do NOT call services outside the local machine (no telemetry, no remote reporting).
|
|
33
|
+
- I do NOT auto-install missing tools — I report the absence and let the user decide.
|
|
34
|
+
|
|
35
|
+
## Hand-off to TEA
|
|
36
|
+
|
|
37
|
+
Hawkeye / TEA may review the security-overlay's own implementation (this code) in the kit's normal gates (risk / design / trace / review / gate). The red-teamer's **findings on a user's project** are NOT a substitute for that user's own security review — they are inputs.
|
|
38
|
+
|
|
39
|
+
When a finding is severity High or Critical, the orchestrator surfaces it with PoC + scope_sha256 + scope mode, so the user can act on it inside their normal review process.
|
|
40
|
+
|
|
41
|
+
## Tom
|
|
42
|
+
|
|
43
|
+
Pragmático. Direto. Sem floreio. Pentester real que respeita o escopo.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
.git
|
|
2
|
+
.git/HEAD
|
|
3
|
+
.git/config
|
|
4
|
+
.env
|
|
5
|
+
.env.local
|
|
6
|
+
.env.backup
|
|
7
|
+
.env.example
|
|
8
|
+
.htaccess
|
|
9
|
+
.htpasswd
|
|
10
|
+
.svn
|
|
11
|
+
.DS_Store
|
|
12
|
+
admin
|
|
13
|
+
administrator
|
|
14
|
+
admin.php
|
|
15
|
+
api
|
|
16
|
+
api/v1
|
|
17
|
+
api/v2
|
|
18
|
+
app
|
|
19
|
+
assets
|
|
20
|
+
backup
|
|
21
|
+
backups
|
|
22
|
+
backup.zip
|
|
23
|
+
backup.sql
|
|
24
|
+
backup.tar.gz
|
|
25
|
+
bin
|
|
26
|
+
cache
|
|
27
|
+
cgi-bin
|
|
28
|
+
composer.json
|
|
29
|
+
composer.lock
|
|
30
|
+
config
|
|
31
|
+
config.php
|
|
32
|
+
config.json
|
|
33
|
+
console
|
|
34
|
+
cron
|
|
35
|
+
css
|
|
36
|
+
dashboard
|
|
37
|
+
data
|
|
38
|
+
db
|
|
39
|
+
debug
|
|
40
|
+
dev
|
|
41
|
+
docs
|
|
42
|
+
download
|
|
43
|
+
downloads
|
|
44
|
+
dump.sql
|
|
45
|
+
error
|
|
46
|
+
error_log
|
|
47
|
+
export
|
|
48
|
+
files
|
|
49
|
+
fonts
|
|
50
|
+
ftp
|
|
51
|
+
health
|
|
52
|
+
healthz
|
|
53
|
+
home
|
|
54
|
+
images
|
|
55
|
+
img
|
|
56
|
+
include
|
|
57
|
+
includes
|
|
58
|
+
index.php
|
|
59
|
+
info.php
|
|
60
|
+
install
|
|
61
|
+
js
|
|
62
|
+
json
|
|
63
|
+
lib
|
|
64
|
+
log
|
|
65
|
+
logs
|
|
66
|
+
login
|
|
67
|
+
logout
|
|
68
|
+
mail
|
|
69
|
+
media
|
|
70
|
+
metrics
|
|
71
|
+
node_modules
|
|
72
|
+
old
|
|
73
|
+
panel
|
|
74
|
+
phpinfo.php
|
|
75
|
+
phpmyadmin
|
|
76
|
+
private
|
|
77
|
+
public
|
|
78
|
+
readme
|
|
79
|
+
readme.md
|
|
80
|
+
register
|
|
81
|
+
robots.txt
|
|
82
|
+
scripts
|
|
83
|
+
secret
|
|
84
|
+
secrets
|
|
85
|
+
server-status
|
|
86
|
+
setup
|
|
87
|
+
sitemap.xml
|
|
88
|
+
sql
|
|
89
|
+
src
|
|
90
|
+
staging
|
|
91
|
+
static
|
|
92
|
+
stats
|
|
93
|
+
status
|
|
94
|
+
storage
|
|
95
|
+
swagger
|
|
96
|
+
swagger.json
|
|
97
|
+
swagger-ui
|
|
98
|
+
sysadmin
|
|
99
|
+
system
|
|
100
|
+
temp
|
|
101
|
+
test
|
|
102
|
+
tests
|
|
103
|
+
tmp
|
|
104
|
+
tools
|
|
105
|
+
upload
|
|
106
|
+
uploads
|
|
107
|
+
user
|
|
108
|
+
users
|
|
109
|
+
vendor
|
|
110
|
+
web.config
|
|
111
|
+
webhook
|
|
112
|
+
wp-admin
|
|
113
|
+
wp-config.php
|
|
114
|
+
wp-login.php
|
|
115
|
+
.well-known/security.txt
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_schema": "OWASP Top 10 (2021) — stable IDs. Used by tagOwasp() to categorize findings from DAST tools. Update with care; downstream rendering depends on these IDs.",
|
|
3
|
+
"categories": [
|
|
4
|
+
{ "id": "A01:2021", "name": "Broken Access Control" },
|
|
5
|
+
{ "id": "A02:2021", "name": "Cryptographic Failures" },
|
|
6
|
+
{ "id": "A03:2021", "name": "Injection" },
|
|
7
|
+
{ "id": "A04:2021", "name": "Insecure Design" },
|
|
8
|
+
{ "id": "A05:2021", "name": "Security Misconfiguration" },
|
|
9
|
+
{ "id": "A06:2021", "name": "Vulnerable and Outdated Components" },
|
|
10
|
+
{ "id": "A07:2021", "name": "Identification and Authentication Failures" },
|
|
11
|
+
{ "id": "A08:2021", "name": "Software and Data Integrity Failures" },
|
|
12
|
+
{ "id": "A09:2021", "name": "Security Logging and Monitoring Failures" },
|
|
13
|
+
{ "id": "A10:2021", "name": "Server-Side Request Forgery (SSRF)" }
|
|
14
|
+
]
|
|
15
|
+
}
|