blitzstrike 1.0.59 → 1.0.64

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 CHANGED
@@ -5,6 +5,75 @@ All notable changes to Blitz Strike are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [1.0.64] — 2026-09-13
9
+
10
+ ### Added — agent-native skills (LLM-driven orchestration doctrine)
11
+
12
+ Six skills that teach the LLM agent how to drive Blitz Strike's deterministic
13
+ tools end-to-end (the "LLM is the brain, Blitz Strike is the hands" model):
14
+
15
+ - `bs-orchestrate-engagement` — the full cycle: scope → recon → analyze →
16
+ verify → review → report, no-babysitting.
17
+ - `bs-scope-gate` — hard scope/authorization gate before any live action.
18
+ - `bs-source-audit` — drive taint + route-confusion + complex-bugs analyzers.
19
+ - `bs-verify-finding` — marker + negative-control live verification.
20
+ - `bs-adversarial-review` — the ten §15 questions + confidence + dedup.
21
+ - `bs-write-report` — submission-ready report (root cause, CVSS, remediation).
22
+
23
+ Plus `docs/ARCHITECTURE-AUDIT.md` — the Mission Control → LLM-driven toolbelt
24
+ classification (TOOL / SKILL / REDUNDANT).
25
+
26
+ ## [1.0.63] — 2026-09-13
27
+
28
+ ### Added — complex-bug detection (`src/complex-bugs.ts`)
29
+
30
+ Three "hard" vulnerability classes beyond source→sink taint:
31
+
32
+ - **Deserialization → POP gadget chain** — `unserialize()` of attacker-controlled
33
+ data when a magic method (`__destruct`/`__wakeup`/`__toString`/`__invoke`/`__call`)
34
+ is present in the file (object injection → RCE pivot).
35
+ - **Type juggling** — loose comparison (`==`/`!=`) of user input against a
36
+ hash/secret (PHP `0e…` collision auth bypass); strict `===` is ignored.
37
+ - **Mass assignment** — `extract()` of request data without `EXTR_SKIP`, or
38
+ `parse_str()` of user input (variable injection).
39
+
40
+ Wired into the `@source` agent as `vulnerability` graph nodes. Added a
41
+ `php-complex` fixture to the enterprise benchmark.
42
+
43
+ ## [1.0.62] — 2026-09-13
44
+
45
+ ### Added — route-confusion & dispatch-abuse detection
46
+
47
+ New vulnerability class distinct from source→sink taint (`src/route-confusion.ts`):
48
+ detects when the routing/dispatch layer lets an attacker reach an unintended
49
+ handler or control which handler runs.
50
+
51
+ - `dynamic_dispatch` — attacker-controlled callback/route (`call_user_func`,
52
+ `forward`, `dispatch`, `resolve_route`, `invoke`).
53
+ - `dynamic_method_call` — `$obj->$method()` / `$class::$method()`.
54
+ - `dynamic_include` — `include`/`require` of a variable (LFI via route).
55
+ - `batch_forwarding` — a loop over sub-requests that forwards to inner handlers
56
+ without a per-route authorization re-check.
57
+ - `route_normalization_gap` — route matching against user input without path
58
+ normalization (strtolower/rtrim/urldecode).
59
+
60
+ Wired into the `@source` agent; findings become `route` nodes in the security
61
+ graph. Added a WordPress-style batch-endpoint fixture to the enterprise
62
+ benchmark (`wp-batch-route`).
63
+
64
+ ## [1.0.60] — 2026-09-13
65
+
66
+ ### Added — deeper taint-flow coverage
67
+
68
+ - **Method-call resolution** (`calleeName`) — `$obj->method()` now resolves to
69
+ its method definition for interprocedural taint, not the object variable.
70
+ - **Built-in taint propagation** — `implode`/`join`/`sprintf`/`str_replace` and
71
+ other array→string/format built-ins now carry taint through to the sink.
72
+ - **Cast handling** — `(array)`/`(string)` casts propagate taint (a string can
73
+ still carry a payload); `(int)`/`(float)` casts act as an int_cast sanitizer.
74
+ - New corpus fixtures covering conditional-sanitizer bypass on array→SQL
75
+ interpolation (`php-int-cast-bypass-vuln`/`-fixed`).
76
+
8
77
  ## [1.0.59] — 2026-09-13
9
78
 
10
79
  ### Added — enterprise-grade detection + benchmark
@@ -0,0 +1,20 @@
1
+ - id: php-int-cast-bypass-fixed
2
+ - language: php
3
+ - vulnerable: false
4
+ - sink_type: sql_execution
5
+ ```php
6
+ <?php
7
+ class Query {
8
+ public function run($params) {
9
+ $where = "";
10
+ if ( ! empty( $params["ids"] ) ) {
11
+ $ids = implode( ",", array_map( "intval", (array) $params["ids"] ) );
12
+ $where .= " AND id NOT IN ($ids) ";
13
+ }
14
+ return $db->query("SELECT * FROM t WHERE 1=1 $where");
15
+ }
16
+ }
17
+ $q = $_GET;
18
+ $w = new Query();
19
+ $w->run($q);
20
+ ```
@@ -0,0 +1,23 @@
1
+ - id: php-int-cast-bypass-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - sink_type: sql_execution
5
+ ```php
6
+ <?php
7
+ class Query {
8
+ public function run($params) {
9
+ $where = "";
10
+ if ( ! empty( $params["ids"] ) ) {
11
+ if ( is_array( $params["ids"] ) ) {
12
+ $params["ids"] = array_map( "intval", $params["ids"] );
13
+ }
14
+ $ids = implode( ",", (array) $params["ids"] );
15
+ $where .= " AND id NOT IN ($ids) ";
16
+ }
17
+ return $db->query("SELECT * FROM t WHERE 1=1 $where");
18
+ }
19
+ }
20
+ $q = $_GET;
21
+ $w = new Query();
22
+ $w->run($q);
23
+ ```
@@ -0,0 +1,34 @@
1
+ <?php
2
+ /**
3
+ * Plugin helper (simplified) with multiple complex-bug patterns.
4
+ */
5
+
6
+ // 1. Deserialization → object injection (magic method present)
7
+ function restore_session($payload) {
8
+ $data = $_POST['data'];
9
+ $obj = unserialize($data);
10
+ return $obj;
11
+ }
12
+
13
+ class CacheManager {
14
+ public function __destruct() {
15
+ // Magic method — a POP gadget can pivot object injection to RCE here.
16
+ system("rm -rf /tmp/cache");
17
+ }
18
+ }
19
+
20
+ // 2. Type juggling — loose comparison of user input against a hash
21
+ function verify_signature() {
22
+ $sig = $_GET['sig'];
23
+ $secret = get_option('signing_secret');
24
+ if ($sig == $secret) { // <-- loose == , 0e... collision bypass
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ // 3. Mass assignment — extract() of request data without EXTR_SKIP
31
+ function bootstrap_options() {
32
+ extract($_REQUEST); // <-- variable injection
33
+ return $admin; // $admin is attacker-controlled
34
+ }
@@ -0,0 +1,43 @@
1
+ <?php
2
+ /**
3
+ * REST API batch endpoint (simplified) — forwards to inner handlers.
4
+ *
5
+ * The `/batch` route is registered WITHOUT a permission_callback, so it is
6
+ * reachable unauthenticated. A malformed nested request desynchronizes the
7
+ * batch router, letting an attacker reach handlers they should not.
8
+ */
9
+
10
+ add_action('rest_api_init', function () {
11
+ register_rest_route('my/v1', '/batch', array(
12
+ 'methods' => 'POST',
13
+ 'callback' => 'handle_batch',
14
+ 'permission_callback' => '__return_true', // <-- no real authz check
15
+ ));
16
+ });
17
+
18
+ function handle_batch($request) {
19
+ $operations = $request['requests'];
20
+ $responses = array();
21
+
22
+ foreach ($operations as $op) {
23
+ $route = $op['route'];
24
+ $handler = resolve_route($route);
25
+ $responses[] = $handler($op['params']);
26
+ }
27
+
28
+ return $responses;
29
+ }
30
+
31
+ function resolve_route($route) {
32
+ $registry = array(
33
+ 'users' => 'get_users',
34
+ 'posts' => 'get_posts',
35
+ 'options' => 'get_options',
36
+ );
37
+ return $registry[$route];
38
+ }
39
+
40
+ function get_users($params) {
41
+ global $wpdb;
42
+ return $wpdb->get_results("SELECT * FROM users WHERE id = " . $params['id']);
43
+ }
package/dist/index.js CHANGED
@@ -17104,6 +17104,15 @@ function rootVarName2(node) {
17104
17104
  return rootVarName2(node.what);
17105
17105
  return null;
17106
17106
  }
17107
+ function calleeName(what) {
17108
+ if (!isNode3(what))
17109
+ return null;
17110
+ if (what.kind === "name")
17111
+ return typeof what.name === "string" ? what.name : what.name?.name ?? null;
17112
+ if (what.kind === "propertylookup" || what.kind === "staticlookup")
17113
+ return what.offset?.name ?? what.offset?.value ?? null;
17114
+ return null;
17115
+ }
17107
17116
  function sourceOf2(node) {
17108
17117
  if (!isNode3(node))
17109
17118
  return null;
@@ -17268,6 +17277,14 @@ function analyzeExpr(node, env, summary, ctx, taintedParam, props, activeAuth, s
17268
17277
  const src = sourceOf2(node);
17269
17278
  if (src)
17270
17279
  return { tainted: true, sources: [src], sanitizers: [], sourceLine: node.loc?.start?.line };
17280
+ if (node.kind === "cast") {
17281
+ const inner = analyzeExpr(node.expr, env, summary, ctx, taintedParam, props, activeAuth, summaries, validating);
17282
+ const castType = String(node.type ?? "");
17283
+ if (/^(int|integer|bool|boolean|float|double)$/i.test(castType) && inner.tainted) {
17284
+ return { ...inner, sanitizers: [...inner.sanitizers, { id: "int_cast", label: "Numeric cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] }] };
17285
+ }
17286
+ return inner;
17287
+ }
17271
17288
  if (node.kind === "assign") {
17272
17289
  const byref = node.operator === "=&";
17273
17290
  const rhs = analyzeExpr(node.right, env, summary, ctx, taintedParam, props, activeAuth, summaries, validating);
@@ -17315,8 +17332,11 @@ function analyzeExpr(node, env, summary, ctx, taintedParam, props, activeAuth, s
17315
17332
  analyzeExpr(a, env, summary, ctx, taintedParam, props, activeAuth, summaries, validating);
17316
17333
  return taintedArgs.length > 0 ? mergeTaint(...taintedArgs) : CLEAN;
17317
17334
  }
17318
- const fnName = rootVarName2(node.what);
17335
+ const fnName = calleeName(node.what);
17319
17336
  if (fnName) {
17337
+ if (TAINT_PROPAGATING_BUILTINS.has(fnName) && taintedArgs.length > 0) {
17338
+ return mergeTaint(...taintedArgs);
17339
+ }
17320
17340
  const san = findSanitizers(fnName + "(");
17321
17341
  if (san.length > 0) {
17322
17342
  if (taintedArgs.length > 0) {
@@ -17575,7 +17595,7 @@ function analyzeDataFlow2(path) {
17575
17595
  const seen = new Set;
17576
17596
  const mainCalls = topStmts.flatMap((s) => findKind2(s, "call"));
17577
17597
  for (const call of mainCalls) {
17578
- const fnName = rootVarName2(call.what);
17598
+ const fnName = calleeName(call.what);
17579
17599
  if (!fnName || sinkClassOf2(call))
17580
17600
  continue;
17581
17601
  const target = resolveFuncId(fnName, summaries);
@@ -17662,7 +17682,7 @@ function analyzeDataFlow2(path) {
17662
17682
  setCache("eagle2", cacheCode, result);
17663
17683
  return result;
17664
17684
  }
17665
- var import_php_parser3, parser3, CLEAN, VALIDATION_GATES;
17685
+ var import_php_parser3, parser3, TAINT_PROPAGATING_BUILTINS, CLEAN, VALIDATION_GATES;
17666
17686
  var init_eagle2 = __esm(() => {
17667
17687
  init_dataflow();
17668
17688
  init_scanner();
@@ -17673,6 +17693,33 @@ var init_eagle2 = __esm(() => {
17673
17693
  parser: { extractDoc: false, php7: true },
17674
17694
  ast: { withPositions: true, withSource: false }
17675
17695
  });
17696
+ TAINT_PROPAGATING_BUILTINS = new Set([
17697
+ "implode",
17698
+ "join",
17699
+ "sprintf",
17700
+ "vsprintf",
17701
+ "str_replace",
17702
+ "str_ireplace",
17703
+ "substr",
17704
+ "mb_substr",
17705
+ "substr_replace",
17706
+ "trim",
17707
+ "ltrim",
17708
+ "rtrim",
17709
+ "strtolower",
17710
+ "strtoupper",
17711
+ "ucfirst",
17712
+ "lcfirst",
17713
+ "urldecode",
17714
+ "rawurldecode",
17715
+ "base64_decode",
17716
+ "html_entity_decode",
17717
+ "stripslashes",
17718
+ "addslashes",
17719
+ "strval",
17720
+ "preg_quote",
17721
+ "nl2br"
17722
+ ]);
17676
17723
  CLEAN = { tainted: false, sources: [], sanitizers: [] };
17677
17724
  VALIDATION_GATES = ["in_array", "preg_match", "ctype_digit", "ctype_alnum", "is_numeric"];
17678
17725
  });
@@ -37002,6 +37049,269 @@ function reviewValidated(mission) {
37002
37049
  }
37003
37050
  var init_reviewer = () => {};
37004
37051
 
37052
+ // src/route-confusion.ts
37053
+ function lines(code) {
37054
+ return code.split(`
37055
+ `);
37056
+ }
37057
+ function lineNo(code, pos) {
37058
+ let n = 1;
37059
+ for (let i = 0;i < pos; i++)
37060
+ if (code.charCodeAt(i) === 10)
37061
+ n++;
37062
+ return n;
37063
+ }
37064
+ function userControlled(scope) {
37065
+ return SOURCE_TOKENS.test(scope);
37066
+ }
37067
+ function detectDynamicDispatch(code, file, out) {
37068
+ const ls = lines(code);
37069
+ DYNAMIC_DISPATCH_RE.lastIndex = 0;
37070
+ let m;
37071
+ while ((m = DYNAMIC_DISPATCH_RE.exec(code)) !== null) {
37072
+ const ln = lineNo(code, m.index);
37073
+ const l = ls[ln - 1] ?? "";
37074
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
37075
+ `);
37076
+ if (!userControlled(l) && !userControlled(scope))
37077
+ continue;
37078
+ out.push({
37079
+ file,
37080
+ line: ln,
37081
+ type: "dynamic_dispatch",
37082
+ category: "Route confusion (user-controlled dispatch)",
37083
+ severity: "high",
37084
+ evidence: l.trim(),
37085
+ detail: `\`${m[1]}()\` invoked with an attacker-controlled callback/route — the request selects the handler.`
37086
+ });
37087
+ }
37088
+ }
37089
+ function detectDynamicMethod(code, file, out) {
37090
+ const ls = lines(code);
37091
+ DYNAMIC_METHOD_RE.lastIndex = 0;
37092
+ let m;
37093
+ while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
37094
+ const ln = lineNo(code, m.index);
37095
+ const l = ls[ln - 1] ?? "";
37096
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
37097
+ `);
37098
+ if (!userControlled(l) && !userControlled(scope))
37099
+ continue;
37100
+ out.push({
37101
+ file,
37102
+ line: ln,
37103
+ type: "dynamic_method_call",
37104
+ category: "Route confusion (dynamic method dispatch)",
37105
+ severity: "high",
37106
+ evidence: l.trim(),
37107
+ detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
37108
+ });
37109
+ }
37110
+ }
37111
+ function detectDynamicInclude(code, file, out) {
37112
+ const ls = lines(code);
37113
+ const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
37114
+ let m;
37115
+ while ((m = re.exec(code)) !== null) {
37116
+ const ln = lineNo(code, m.index);
37117
+ const l = ls[ln - 1] ?? "";
37118
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
37119
+ `);
37120
+ if (!userControlled(l) && !userControlled(scope))
37121
+ continue;
37122
+ out.push({
37123
+ file,
37124
+ line: ln,
37125
+ type: "dynamic_include",
37126
+ category: "Route confusion (dynamic file include)",
37127
+ severity: "high",
37128
+ evidence: l.trim(),
37129
+ detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
37130
+ });
37131
+ }
37132
+ }
37133
+ function detectBatchForwarding(code, file, out) {
37134
+ BATCH_LOOP_RE.lastIndex = 0;
37135
+ let m;
37136
+ while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
37137
+ const ln = lineNo(code, m.index);
37138
+ const body = lines(code).slice(ln, ln + 8).join(`
37139
+ `);
37140
+ const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
37141
+ if (!forwards)
37142
+ continue;
37143
+ const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
37144
+ out.push({
37145
+ file,
37146
+ line: ln,
37147
+ type: "batch_forwarding",
37148
+ category: "Route confusion (batch/proxy forwarding)",
37149
+ severity: rechecks ? "medium" : "high",
37150
+ evidence: lines(code)[ln - 1]?.trim() ?? "",
37151
+ detail: rechecks ? "Batch/proxy loop forwards to inner handlers — auth is re-checked in scope, but verify it applies per-route." : "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
37152
+ });
37153
+ }
37154
+ }
37155
+ function detectRouteNormalization(code, file, out) {
37156
+ const ls = lines(code);
37157
+ for (let i = 0;i < ls.length; i++) {
37158
+ const l = ls[i];
37159
+ if (!ROUTE_MATCH_RE.test(l))
37160
+ continue;
37161
+ const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
37162
+ `);
37163
+ if (!userControlled(scope))
37164
+ continue;
37165
+ if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
37166
+ continue;
37167
+ out.push({
37168
+ file,
37169
+ line: i + 1,
37170
+ type: "route_normalization_gap",
37171
+ category: "Route confusion (missing path normalization)",
37172
+ severity: "medium",
37173
+ evidence: l.trim(),
37174
+ detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
37175
+ });
37176
+ }
37177
+ }
37178
+ function detectRouteConfusion(code, file) {
37179
+ const out = [];
37180
+ detectDynamicDispatch(code, file, out);
37181
+ detectDynamicMethod(code, file, out);
37182
+ detectDynamicInclude(code, file, out);
37183
+ detectBatchForwarding(code, file, out);
37184
+ detectRouteNormalization(code, file, out);
37185
+ return out;
37186
+ }
37187
+ var SOURCE_TOKENS, DYNAMIC_DISPATCH_RE, DYNAMIC_METHOD_RE, BATCH_LOOP_RE, ROUTE_MATCH_RE, NORMALIZERS;
37188
+ var init_route_confusion = __esm(() => {
37189
+ SOURCE_TOKENS = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post)|getParameter\(|req\.(query|body|params)/;
37190
+ DYNAMIC_DISPATCH_RE = /\b(call_user_func|call_user_func_array|forward|dispatch|resolve_route|invoke)\s*\(\s*(\$|\[)/gi;
37191
+ DYNAMIC_METHOD_RE = /(->|::)\s*\$[A-Za-z_]/g;
37192
+ BATCH_LOOP_RE = /\bforeach\s*\([^)]*(requests|batch|sub_requests|operations)\b/gi;
37193
+ ROUTE_MATCH_RE = /\b(preg_match|preg_match_all|strpos|stripos|str_contains)\s*\(/i;
37194
+ NORMALIZERS = /\b(strtolower|strtoupper|rtrim|urldecode|rawurldecode|parse_url|normalize|canonical)\b/i;
37195
+ });
37196
+
37197
+ // src/complex-bugs.ts
37198
+ function lines2(code) {
37199
+ return code.split(`
37200
+ `);
37201
+ }
37202
+ function lineNo2(code, pos) {
37203
+ let n = 1;
37204
+ for (let i = 0;i < pos; i++)
37205
+ if (code.charCodeAt(i) === 10)
37206
+ n++;
37207
+ return n;
37208
+ }
37209
+ function detectDeserialization(code, file, out) {
37210
+ if (!MAGIC_METHODS.test(code))
37211
+ return;
37212
+ const ls = lines2(code);
37213
+ const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
37214
+ let m;
37215
+ while ((m = re.exec(code)) !== null) {
37216
+ const ln = lineNo2(code, m.index);
37217
+ const l = ls[ln - 1] ?? "";
37218
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
37219
+ `);
37220
+ if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
37221
+ continue;
37222
+ out.push({
37223
+ file,
37224
+ line: ln,
37225
+ type: "deserialization",
37226
+ category: "Deserialization → POP gadget chain",
37227
+ severity: "high",
37228
+ evidence: l.trim(),
37229
+ detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
37230
+ });
37231
+ }
37232
+ }
37233
+ function detectTypeJuggling(code, file, out) {
37234
+ const ls = lines2(code);
37235
+ const re = /(?<![=!<>])(==|!=)(?!=)/g;
37236
+ let m;
37237
+ while ((m = re.exec(code)) !== null) {
37238
+ const ln = lineNo2(code, m.index);
37239
+ const l = ls[ln - 1] ?? "";
37240
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
37241
+ `);
37242
+ const hasSource = SOURCE_TOKENS2.test(l) || SOURCE_TOKENS2.test(scope);
37243
+ const hasHash = /\b(md5|sha1|sha256|hash|crc32|crypt|password_verify|strcmp|hash_equals|secret|token|nonce)\s*\(?/i.test(l) || /\b(secret|token|nonce|hash)\b/i.test(l);
37244
+ if (!hasSource || !hasHash)
37245
+ continue;
37246
+ out.push({
37247
+ file,
37248
+ line: ln,
37249
+ type: "type_juggling",
37250
+ category: "Type juggling (loose comparison)",
37251
+ severity: "medium",
37252
+ evidence: l.trim(),
37253
+ detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
37254
+ });
37255
+ }
37256
+ }
37257
+ function detectMassAssignment(code, file, out) {
37258
+ const ls = lines2(code);
37259
+ const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
37260
+ let m;
37261
+ while ((m = extractRe.exec(code)) !== null) {
37262
+ const ln = lineNo2(code, m.index);
37263
+ const l = ls[ln - 1] ?? "";
37264
+ if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
37265
+ continue;
37266
+ out.push({
37267
+ file,
37268
+ line: ln,
37269
+ type: "mass_assignment",
37270
+ category: "Mass assignment (variable injection)",
37271
+ severity: "high",
37272
+ evidence: l.trim(),
37273
+ detail: "`extract()` of request data without EXTR_SKIP — attacker can overwrite arbitrary variables."
37274
+ });
37275
+ }
37276
+ const parseRe = /\bparse_str\s*\(/g;
37277
+ while ((m = parseRe.exec(code)) !== null) {
37278
+ const ln = lineNo2(code, m.index);
37279
+ const l = ls[ln - 1] ?? "";
37280
+ const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
37281
+ `);
37282
+ if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
37283
+ continue;
37284
+ out.push({
37285
+ file,
37286
+ line: ln,
37287
+ type: "mass_assignment",
37288
+ category: "Mass assignment (variable injection)",
37289
+ severity: "medium",
37290
+ evidence: l.trim(),
37291
+ detail: "`parse_str()` of user input into the symbol table — variable injection."
37292
+ });
37293
+ }
37294
+ }
37295
+ function detectComplexBugs(code, file) {
37296
+ const out = [];
37297
+ detectDeserialization(code, file, out);
37298
+ detectTypeJuggling(code, file, out);
37299
+ detectMassAssignment(code, file, out);
37300
+ const seen = new Set;
37301
+ return out.filter((f) => {
37302
+ const key = `${f.type}:${f.line}`;
37303
+ if (seen.has(key))
37304
+ return false;
37305
+ seen.add(key);
37306
+ return true;
37307
+ });
37308
+ }
37309
+ var SOURCE_TOKENS2, MAGIC_METHODS;
37310
+ var init_complex_bugs = __esm(() => {
37311
+ SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post|data)|getParameter\(|req\.(query|body|params)/;
37312
+ MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
37313
+ });
37314
+
37005
37315
  // src/agents.ts
37006
37316
  import { readFileSync as readFileSync18 } from "node:fs";
37007
37317
  function emptyGraph() {
@@ -37025,6 +37335,8 @@ var init_agents = __esm(() => {
37025
37335
  init_scanner();
37026
37336
  init_eagle2();
37027
37337
  init_universal_taint();
37338
+ init_route_confusion();
37339
+ init_complex_bugs();
37028
37340
  init_adapters();
37029
37341
  init_active();
37030
37342
  init_strike();
@@ -37109,6 +37421,44 @@ var init_agents = __esm(() => {
37109
37421
  graph = addEdge2(graph, tid, sinkId, "flows_to");
37110
37422
  }
37111
37423
  }
37424
+ for (const f of files.filter((x) => /\.php$/i.test(x))) {
37425
+ let code;
37426
+ try {
37427
+ code = readFileSync18(f, "utf8");
37428
+ } catch {
37429
+ continue;
37430
+ }
37431
+ for (const rc of detectRouteConfusion(code, f).slice(0, 50)) {
37432
+ const hyp = makeHypothesis({
37433
+ type: rc.category,
37434
+ target: `${f}:${rc.line}`,
37435
+ evidence: rc.detail,
37436
+ confidence: rc.severity === "high" ? 0.7 : 0.5,
37437
+ required_validation: "confirm reachability of the dispatch/route with attacker-controlled input",
37438
+ potential_impact: "route confusion / authorization bypass"
37439
+ });
37440
+ hypotheses.push(hyp);
37441
+ findings += 1;
37442
+ const nodeId = `route:${f}:${rc.line}:${rc.type}`;
37443
+ graph = upsertNode(graph, nodeId, "route", { file: f, line: rc.line, type: rc.type, hypothesis: hyp.id });
37444
+ graph = addEdge2(graph, tid, nodeId, "routes_to");
37445
+ }
37446
+ for (const cb of detectComplexBugs(code, f).slice(0, 50)) {
37447
+ const hyp = makeHypothesis({
37448
+ type: cb.category,
37449
+ target: `${f}:${cb.line}`,
37450
+ evidence: cb.detail,
37451
+ confidence: cb.severity === "high" ? 0.7 : 0.55,
37452
+ required_validation: "confirm the payload reaches the sink and is exploitable",
37453
+ potential_impact: cb.type
37454
+ });
37455
+ hypotheses.push(hyp);
37456
+ findings += 1;
37457
+ const nodeId = `complex:${f}:${cb.line}:${cb.type}`;
37458
+ graph = upsertNode(graph, nodeId, "vulnerability", { file: f, line: cb.line, type: cb.type, hypothesis: hyp.id });
37459
+ graph = addEdge2(graph, tid, nodeId, "vulnerable_to");
37460
+ }
37461
+ }
37112
37462
  return { ok: true, output: { files: files.length, findings }, hypotheses, graph };
37113
37463
  }
37114
37464
  };
@@ -38044,6 +38394,20 @@ var init_enterprise_benchmark = __esm(() => {
38044
38394
  expected_vulns: 2,
38045
38395
  expected_sinks: ["sql_execution", "command_execution"],
38046
38396
  safe_tokens: ["prepareStatement", "PreparedStatement"]
38397
+ },
38398
+ {
38399
+ id: "wp-batch-route",
38400
+ dir: join17(ROOT10, "wp-batch-route"),
38401
+ expected_vulns: 1,
38402
+ expected_sinks: ["Route confusion"],
38403
+ safe_tokens: []
38404
+ },
38405
+ {
38406
+ id: "php-complex",
38407
+ dir: join17(ROOT10, "php-complex"),
38408
+ expected_vulns: 4,
38409
+ expected_sinks: ["Deserialization", "Type juggling", "Mass assignment"],
38410
+ safe_tokens: []
38047
38411
  }
38048
38412
  ];
38049
38413
  });
@@ -0,0 +1,88 @@
1
+ # Architecture Audit — Mission Control → LLM-Driven Toolbelt
2
+
3
+ Step 1 of the OMOP-alignment refactor. Every Mission Control component is
4
+ classified into one of:
5
+
6
+ - **TOOL** — a deterministic check the LLM calls via MCP (produces a result the
7
+ LLM reasons over).
8
+ - **SKILL** — "how to run the pentest" doctrine/knowledge the LLM loads
9
+ (becomes `SKILL.md`).
10
+ - **DATA** — static knowledge (patterns/chains/correlations) kept as-is.
11
+ - **REDUNDANT** — deterministic orchestration that duplicates what the LLM
12
+ already does; remove or fold into a TOOL/SKILL.
13
+
14
+ The guiding principle: **the LLM is the brain; Blitz Strike is the deterministic
15
+ hands + knowledge + guardrails.** A deterministic "self-driving loop" that tries
16
+ to BE the brain is the anti-pattern being removed.
17
+
18
+ ---
19
+
20
+ ## src/mission/ — classification
21
+
22
+ | File | Component | Verdict | Why |
23
+ |------|-----------|---------|-----|
24
+ | `types.ts` | shared types (Mission/Task/Hypothesis/Budget/Graph) | **DATA** | Type contracts. Reshape `Mission`→`Engagement` later, but keep as the shared layer. |
25
+ | `state.ts` | `createMission`/`save`/`load`/`list`/`delete` | **TOOL** | Persistence is useful — repurpose as *engagement/finding state* the LLM drives, not an "autonomous mission" blob. |
26
+ | `taskgraph.ts` | `schedule`/`nextBatch`/`allResolved` | **REDUNDANT** | Task scheduling is the LLM's plan-agent job. |
27
+ | `graph.ts` | Security Graph (nodes/edges/stats/merge) | **TOOL** | The finding-relationship graph is genuinely useful for the LLM to track + query. |
28
+ | `hypothesis.ts` | Hypothesis Engine (make/validate/reject/merge) | **TOOL** | Core moat — evidence-first `observation→hypothesis→validated→rejected` lifecycle. LLM calls these per finding. |
29
+ | `control.ts` | `runMission` loop + `buildInitialTasks` + `replan` | **REDUNDANT** | The self-driving scheduler loop — the LLM drives this instead. |
30
+ | `control.ts` | `scopeAllowed` | **TOOL** | Scope guardrail (keep + expose as a check the LLM MUST call before live actions). |
31
+ | `planner.ts` | `ATTACK_BRANCHES` + escalation graph | **SKILL** | Knowledge: which attack branches/escapes exist. Becomes `SKILL.md` + data. |
32
+ | `planner.ts` | `planNextActions` (ranking) | **TOOL** | Deterministic confidence×value−risk ranking the LLM can use as a hint. |
33
+ | `coverage.ts` | `computeCoverage`/`coverageGaps` | **TOOL** | LLM calls to find un-covered dimensions. `COVERAGE_DIMENSIONS` → **SKILL**. |
34
+ | `recovery.ts` | `diagnoseError`/`recoveryDecision` | **TOOL** | LLM calls on failure to pick retry/alternative. `AGENT_ALTERNATIVES` → **SKILL**. |
35
+ | `memory.ts` | `persistMissionMemory`/`recallForTarget` | **TOOL** | Cross-session memory (keep — the LLM's long-term finding store). |
36
+ | `reviewer.ts` | `reviewFinding`/`reviewValidated` (10 questions) | **TOOL** | Adversarial reviewer — key moat. LLM calls before CONFIRMING a finding. |
37
+ | `router.ts` | `routeTask`/`AGENT_TIER` | **REDUNDANT** | Model routing is the LLM platform's own job (or a small TOOL hint). |
38
+ | `budget.ts` | `budgetStatus`/`budgetOverrun`/`consumeTasks` | **TOOL** | LLM calls to check budget. The "terminate on overrun" auto-decision is **REDUNDANT**. |
39
+ | `termination.ts` | `evaluateTermination`/`applyTermination` | **REDUNDANT** | The LLM decides when to stop; the 7 reasons → **SKILL** doctrine. |
40
+ | `benchmark.ts` + `enterprise-benchmark.ts` | benchmark runners | **DATA/QA** | Keep — not runtime brain; QA harness. |
41
+ | `cli.ts` | `mission` CLI subcommands | **REDUNDANT** | Deterministic-brain CLI. Optional standalone fallback; LLM drives via MCP. |
42
+ | `mcp.ts` | `registerMissionTools` (mission_start/status/…) | **REDUNDANT** | Replace with skill/tool MCP tools (the LLM drives granular tools). |
43
+
44
+ ## src/agents.ts — 13 agents
45
+
46
+ Each agent is a deterministic executor = orchestration (REDUNDANT) + detection
47
+ calls (TOOL). The agent's *responsibility + tool list* becomes a **SKILL**.
48
+
49
+ | Agent | Repurpose as SKILL | Kept as TOOL |
50
+ |-------|--------------------|--------------|
51
+ | `commander` | orchestration doctrine (plan → delegate → verify → report) | — (LLM is the commander) |
52
+ | `recon` | passive recon doctrine | `activeScan`/`live_recon` (already MCP tools) |
53
+ | `web` | live web-attack doctrine | `strike_verify`/`browser_validate` |
54
+ | `api` | API recon/authz doctrine | `api` detection tools |
55
+ | `source` | source-audit doctrine | `analyzeDataFlow2` + `detectRouteConfusion` + `detectComplexBugs` |
56
+ | `auth` | auth-gap doctrine | auth detection |
57
+ | `analysis` | multi-language taint doctrine | `analyzeTaintUniversal` |
58
+ | `validator` | verification doctrine (marker + negative) | `strikeVerify` |
59
+ | `reviewer` | adversarial-review doctrine | `reviewFinding` (TOOL above) |
60
+ | `reporter` | report-writing doctrine | report generator |
61
+ | `browser` | browser-session doctrine | `browser-agent.ts` (session ops) |
62
+ | `mobile` | mobile RE doctrine | mobile analysis |
63
+ | `cloud` | cloud-enum doctrine | cloud tools |
64
+
65
+ ## src/browser-agent.ts
66
+
67
+ Session-based browser automation → **TOOL** (LLM calls `browser_agent` op).
68
+ Keep as-is.
69
+
70
+ ## src/server.ts (69 MCP tools)
71
+
72
+ **KEEP** — the deterministic analysis/verification/evidence tools are the moat.
73
+ Add `instructions` + `next_steps` per tool for LLM ergonomics.
74
+
75
+ ---
76
+
77
+ ## Net outcome
78
+
79
+ - **Keep as TOOL (moat):** taint engine, universal taint, route-confusion,
80
+ complex-bugs, strike_verify, hypothesis engine, security graph, adversarial
81
+ reviewer, coverage, recovery, budget-status, memory, scope gate, browser agent.
82
+ - **Become SKILL:** 13 agent doctrines, reviewer 10-questions, coverage
83
+ dimensions, attack branches, escalation graph, termination reasons, recovery
84
+ alternatives.
85
+ - **Remove (REDUNDANT):** `runMission` loop, task scheduler, replan, model
86
+ router, termination auto-decision, `mission` CLI, `registerMissionTools`.
87
+
88
+ The deterministic analysis stays; the deterministic *orchestration* goes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "1.0.59",
3
+ "version": "1.0.64",
4
4
  "description": "Blitz Strike — a universal MCP security-audit toolbelt. BLITZ sweeps the attack surface, EAGLE-EYE traces source-to-sink, STRIKE verifies live. 57 attack chains, 130-tool catalog, intelligence data layer. One server, every agent.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: adversarial-review
3
+ description: Use before confirming a verified finding as real. Run the ten adversarial questions, score confidence, dedup shared-root findings, and reject anything that does not survive. This is the anti-hallucination gate between "hypothesis" and "confirmed finding".
4
+ ---
5
+
6
+ # Adversarial review (anti-hallucination gate)
7
+
8
+ "A hypothesis is not a finding. AI reasoning is not evidence. Severity is not
9
+ confidence." Before any finding is confirmed, attack it from every angle.
10
+
11
+ ## The ten questions (§15)
12
+
13
+ For each finding, answer these with evidence — not vibes:
14
+
15
+ 1. Is the vulnerable code actually **reachable**?
16
+ 2. Is **authentication** required (and did we account for it)?
17
+ 3. Is **authorization** enforced elsewhere (would the attacker be blocked)?
18
+ 4. Is there **middleware/WAF/sanitizer** protection?
19
+ 5. Is the input actually **attacker-controlled**?
20
+ 6. Is the **sink** reachable from that input?
21
+ 7. Is the observed behavior just **caching** (ETag/304)?
22
+ 8. Is the exploit **reproducible** (marker confirmed, not one-off)?
23
+ 9. Does the **negative control** remain inert?
24
+ 10. Is the **impact** accurately classified (CWE/CVSS, not inflated)?
25
+
26
+ ## Verdict
27
+
28
+ - Any **fail** → REJECTED. Drop it, do not soften it.
29
+ - Fewer than 3 clear **passes** → NEEDS_MORE_EVIDENCE. Go back to `verify-finding`.
30
+ - 3+ passes, 0 fails → CONFIRMED.
31
+
32
+ ## Scoring + dedup
33
+
34
+ - `confidence_score` — deterministic weighted confidence (evidence, verification,
35
+ reachability). Do not invent a confidence number.
36
+ - `dedup_findings` — collapse findings that share a root cause before reporting.
37
+ - `finding_transition` — advance only via the strict lifecycle.
38
+
39
+ ## Why
40
+
41
+ A false positive costs the user their reputation. Rejecting a real bug is
42
+ recoverable (it stays in memory); reporting a fake one is not.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: orchestrate-engagement
3
+ description: Use when the user asks for a full security engagement ("fullscan", "audit this", "pentest this", "scan my target") or any end-to-end assessment. You are the orchestrator — Blitz Strike provides the deterministic analysis and verification tools you drive. Run recon → analyze → verify → review → report without babysitting.
4
+ ---
5
+
6
+ # Orchestrate an engagement (no-babysitting)
7
+
8
+ You are the brain. Blitz Strike is the hands. Drive the full cycle with the
9
+ deterministic MCP tools; never guess a finding — produce it from tool output.
10
+
11
+ ## The cycle
12
+
13
+ 1. **Scope gate (always first).** Call `scope_check` before any live action.
14
+ A target outside scope or without authorization → refuse, record, stop.
15
+ Static source paths on disk are always in-scope; live URLs require an
16
+ explicit scope.
17
+
18
+ 2. **Recon (passive-first).** For a live target: `live_recon` (fingerprint, WAF,
19
+ endpoints, tech). For a source tree: `blitz_scan` (entry points + sinks),
20
+ `detect_framework`, `list_attack_vectors`.
21
+
22
+ 3. **Analyze.** Source → `source-audit` skill (taint + route-confusion +
23
+ complex-bugs). Live → `active_scan` / `enrich_scan`. Consult `tech_correlation`
24
+ / `cve_correlation` / `nvd_lookup` to map tech → known weakness classes.
25
+
26
+ 4. **Verify.** Every hypothesis must pass `verify-finding` before it is a finding.
27
+ "Detection is not proof."
28
+
29
+ 5. **Review.** Every verified finding passes `adversarial-review` before it is
30
+ confirmed. "A hypothesis is not a finding."
31
+
32
+ 6. **Report.** Emit `generate_report` + `cvss_score` + `write-report`.
33
+
34
+ ## Rules
35
+
36
+ - One hypothesis per finding; `finding_create` + `finding_transition` for the
37
+ strict lifecycle (candidate → confirmed → rejected).
38
+ - `dedup_findings` before reporting — collapse shared-root duplicates.
39
+ - Persist hard-won knowledge with `remember`; recall it with `memory_lookup`
40
+ before re-discovering the same target/pattern.
41
+ - You are the orchestrator — do not try to reimplement planning in prose; pick
42
+ tools, act, and let tool output drive the next step.
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: scope-gate
3
+ description: Use BEFORE any live action against a target (recon, scanning, exploitation, verification). Enforce scope and authorization boundaries — never touch a target outside the declared scope, and never run live actions against a target the user has not authorized.
4
+ ---
5
+
6
+ # Scope gate
7
+
8
+ Scope is non-negotiable and cannot be bypassed. Before any live HTTP request,
9
+ port scan, or exploitation step, resolve the boundary deterministically.
10
+
11
+ ## Procedure
12
+
13
+ 1. Call `scope_check` with the target. It returns in-scope / out-of-scope / needs-authorization.
14
+
15
+ 2. **Local source paths** are always in-scope (static analysis of code on disk).
16
+ Live URLs, IPs, and domains require an explicit, user-declared scope.
17
+
18
+ 3. If `scope_check` says out-of-scope or authorization is missing → **refuse and
19
+ record.** Do not probe, do not "just check one thing," do not find a bypass.
20
+
21
+ ## Red lines
22
+
23
+ - Malicious / third-party infrastructure ("cari pelaku") → hold, static analysis
24
+ + takedown only. Never live-probe.
25
+ - Authorized research targets → live action is fine, but only within the scope
26
+ the user declared.
27
+ - A finding that depends on out-of-scope action is not a finding — drop it.
28
+
29
+ ## Why
30
+
31
+ "Scope cannot be bypassed." A tool that ignores scope is worse than a tool that
32
+ misses a bug. The gate is a hard invariant, not a suggestion.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: source-audit
3
+ description: Use when auditing source code (PHP/JS/Python/Java/etc.) for vulnerabilities — static source→sink taint, route confusion/dispatch abuse, deserialization, type juggling, and mass assignment. This is Blitz Strike's deterministic static-analysis moat, distinct from live/dynamic testing.
4
+ ---
5
+
6
+ # Source audit (deterministic static analysis)
7
+
8
+ Drive Blitz Strike's static analyzers over a source tree and turn each hit into a
9
+ hypothesis. Do not hand-guess; let the tools produce the source→sink evidence.
10
+
11
+ ## Workflow
12
+
13
+ 1. **Map the tree.** `blitz_scan <root>` — entry points (nopriv hooks, REST
14
+ routes, controllers) + sink inventory per file. `detect_framework` per file
15
+ to know what conventions apply.
16
+
17
+ 2. **Trace data flow.** For each source→sink candidate:
18
+ - `taint_file` / `taint_scan` — variable-level taint tracking per file.
19
+ - `eagle_eye2` — whole-program interprocedural taint (PHP, method-call aware).
20
+ - `trace_data_flow` — classify sources vs sinks across a file.
21
+ - `variant_analysis` — group data-flow candidates by shared root cause.
22
+
23
+ 3. **Escalate.** `enrich_scan` — match detected sinks to escalation chains
24
+ (SQLi → RCE, upload → shell, LFI → RCE). `list_chains` to see the full
25
+ escalation taxonomy. `tech_correlation` for framework-specific weaknesses.
26
+
27
+ 4. **Deep/complex classes.** Beyond plain taint, look for:
28
+ - **Route confusion** — `call_user_func`, `$obj->$method()`, batch/proxy
29
+ forwarding, dynamic include, route match without normalization.
30
+ - **Deserialization → POP chain** — `unserialize` + `__destruct`/`__wakeup`/
31
+ `__toString` present.
32
+ - **Type juggling** — loose `==`/`!=` of user input vs hash/secret.
33
+ - **Mass assignment** — `extract()` without EXTR_SKIP, `parse_str()` of input.
34
+
35
+ 5. **Correlate.** `cve_correlation` + `nvd_lookup` for the framework/pattern to
36
+ confirm the weakness class is real and known.
37
+
38
+ ## Pitfalls
39
+
40
+ - A sink is not a finding — a sink **reached by attacker-controlled input** is a
41
+ hypothesis. Check the source provenance before reporting.
42
+ - Framework accessors are sources too (`$request->input()`, `request.GET`,
43
+ `@RequestParam`), not just `$_GET`.
44
+ - Parameterized queries, integer casts (`intval`/`absint`/`(int)`), and ORM calls
45
+ are neutralizers — do not flag a sink that is actually sanitized.
46
+
47
+ ## Output
48
+
49
+ Every confirmed sink → `finding_create` with the source→sink chain as evidence,
50
+ then `finding_transition` candidate → validated after `verify-finding`.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: verify-finding
3
+ description: Use when you have a hypothesis and need to prove it is a real, exploitable finding before reporting it. Live verification with a marker + negative control — never report an unverified hypothesis as a finding.
4
+ ---
5
+
6
+ # Verify a finding (marker + negative control)
7
+
8
+ "Detection is not proof." A static hit or a scan match is a hypothesis until it
9
+ is live-verified with a distinguishable marker and an inert negative control.
10
+
11
+ ## Procedure
12
+
13
+ 1. **Pick the right verifier.**
14
+ - Live HTTP → `strike_verify` (sends a payload, checks for a unique marker).
15
+ - Browser/DOM (XSS, auth flows) → `browser_validate`.
16
+
17
+ 2. **Marker, not echo.** Use a unique, nonce-like marker (`blitzstrike-<rand>`)
18
+ that cannot be a benign echo. If the response reflects it, that is the signal.
19
+
20
+ 3. **Negative control.** Send a payload that is structurally similar but MUST NOT
21
+ trigger (a wrong param, a harmless value). If the control ALSO reflects the
22
+ marker, it is a `false_positive` — the app echoes everything, not exploitable.
23
+
24
+ 4. **Interpret.**
25
+ - Marker reflected + control inert → `verdict=confirmed`, attach evidence.
26
+ - Marker reflected + control also reflected → `false_positive`, reject.
27
+ - Nothing → retry once, then `NEEDS_MORE_EVIDENCE`.
28
+
29
+ 5. **Record.** `finding_attach_evidence` (redacted, SHA-256 tagged) +
30
+ `finding_transition` to validated/rejected. Use `redact` on any secret before
31
+ it is persisted.
32
+
33
+ ## Pitfalls
34
+
35
+ - Never verify by "looks right" — the marker/control pair is the only proof.
36
+ - A blind RCE without a callback channel: tunnel the callback (public tunnel,
37
+ no local IP) and observe it arrive — that is the proof.
38
+ - Keep evidence redacted; raw secrets must never be persisted.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: write-report
3
+ description: Use when finalizing confirmed findings into a submission-ready report (bug bounty, pentest deliverable, or audit). Produce a report with root cause, reproduction, CVSS, impact, and remediation — indistinguishable from a top-tier human hunter.
4
+ ---
5
+
6
+ # Write the report
7
+
8
+ Turn confirmed findings into a submission-ready deliverable. A triager should
9
+ not be able to tell an agent wrote it.
10
+
11
+ ## Structure per finding
12
+
13
+ 1. **Title + summary** — one line, precise, no clickbait.
14
+ 2. **Root cause** — the exact code path (source → sink), not a generic description.
15
+ 3. **Reproduction steps** — exact request(s) + the marker/control proof from
16
+ `verify-finding`.
17
+ 4. **Impact** — what an attacker actually gains (data, RCE, account takeover).
18
+ 5. **CVSS** — `cvss_score` with the correct vector string; do not inflate.
19
+ 6. **Remediation** — concrete, implementable, framework-appropriate.
20
+
21
+ ## Tools
22
+
23
+ - `generate_report` — deterministic markdown/JSON report from the findings.
24
+ - `cvss_score` — deterministic CVSS v3.1 vector.
25
+ - `redact` — strip passwords/keys/tokens/cookies before any output.
26
+ - `finding_attach_evidence` — link the redacted, SHA-256-tagged evidence.
27
+
28
+ ## Rules
29
+
30
+ - Only CONFIRMED findings (passed `adversarial-review`) go in the report.
31
+ - No false-positive spam, no "low-hanging fruit" filler.
32
+ - Never include raw secrets, API keys, or live session tokens.
33
+ - Group by root cause; a report full of duplicates reads as noise.