blitzstrike 2.4.35 → 2.4.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/index.js +139 -99
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ 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
+ ## [2.4.37] — 2026-09-14
9
+
10
+ ### Fixed — benchmark top-level recall/f1 (self-audit)
11
+
12
+ - `run_benchmark` now returns top-level `recall` and `f1` (previously only
13
+ `detection_rate` held the recall value, and no top-level f1). Matches the
14
+ per-detector shape for consistency. No functional change.
15
+
16
+ ## [2.4.36] — 2026-09-14
17
+
18
+ ### Changed — Framework Knowledge Graph is now MULTI-framework (not WordPress-only)
19
+
20
+ - `framework-security.ts` now covers 9 frameworks: WordPress, Laravel, Django,
21
+ Flask, Express, Spring, Symfony, ASP.NET Core, Ruby on Rails — each with its
22
+ auth / nonce / entry-point / sanitizer primitives as data.
23
+ - `detectMissingAuthz` now detects missing authorization across ALL frameworks
24
+ (route middleware, decorators, annotations), not just WP AJAX/REST hooks.
25
+ - `framework_knowledge` tool returns the framework list on an unknown id.
26
+
8
27
  ## [2.4.35] — 2026-09-14
9
28
 
10
29
  ### Added — Variant Mining (confirm once, weaponize everywhere)
package/dist/index.js CHANGED
@@ -56260,52 +56260,94 @@ function detectWrongSanitizer(code, file) {
56260
56260
  }
56261
56261
 
56262
56262
  // src/framework-security.ts
56263
- var WP_PRIMITIVES = {
56264
- framework: "wordpress",
56265
- auth_primitives: [
56266
- "current_user_can",
56267
- "current_user_can_edit_post",
56268
- "current_user_can_for_blog",
56269
- "current_user_can_edit_others_posts",
56270
- "is_user_logged_in",
56271
- "authorize"
56272
- ],
56273
- nonce_primitives: [
56274
- "wp_verify_nonce",
56275
- "check_ajax_referer",
56276
- "check_admin_referer",
56277
- "verify_nonce"
56278
- ],
56279
- entry_point_hooks: [
56280
- "wp_ajax_",
56281
- "wp_ajax_nopriv_",
56282
- "admin_post_",
56283
- "admin_post_nopriv_",
56284
- "register_rest_route",
56285
- "add_shortcode",
56286
- "init",
56287
- "rest_api_init"
56288
- ],
56289
- sanitizers: [
56290
- "sanitize_text_field",
56291
- "esc_sql",
56292
- "esc_html",
56293
- "esc_attr",
56294
- "esc_url",
56295
- "wp_kses",
56296
- "wp_kses_post",
56297
- "absint",
56298
- "sanitize_email",
56299
- "sanitize_file_name"
56300
- ]
56301
- };
56302
56263
  var FRAMEWORKS = {
56303
- wordpress: WP_PRIMITIVES,
56304
- wp: WP_PRIMITIVES
56264
+ wordpress: {
56265
+ framework: "wordpress",
56266
+ auth_primitives: ["current_user_can", "current_user_can_edit_post", "is_user_logged_in", "authorize"],
56267
+ nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer", "verify_nonce"],
56268
+ entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "admin_post_", "admin_post_nopriv_", "register_rest_route", "add_shortcode", "init", "rest_api_init"],
56269
+ sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "esc_attr", "esc_url", "wp_kses", "wp_kses_post", "absint", "sanitize_email", "sanitize_file_name"]
56270
+ },
56271
+ wp: {
56272
+ framework: "wordpress",
56273
+ auth_primitives: ["current_user_can", "is_user_logged_in", "authorize"],
56274
+ nonce_primitives: ["wp_verify_nonce", "check_ajax_referer", "check_admin_referer"],
56275
+ entry_point_hooks: ["wp_ajax_", "wp_ajax_nopriv_", "register_rest_route"],
56276
+ sanitizers: ["sanitize_text_field", "esc_sql", "esc_html", "absint"]
56277
+ },
56278
+ laravel: {
56279
+ framework: "laravel",
56280
+ auth_primitives: ["auth", "authorize", "Gate::", "can:", "->middleware('auth'", "->can(", "$this->authorize", "@can", "@cannot", "policy"],
56281
+ nonce_primitives: ["@csrf", "csrf_field", "VerifyCsrfToken"],
56282
+ entry_point_hooks: ["Route::get", "Route::post", "Route::put", "Route::delete", "Route::resource", "Route::match"],
56283
+ sanitizers: ["e(", "htmlspecialchars", "clean(", "strip_tags"]
56284
+ },
56285
+ django: {
56286
+ framework: "django",
56287
+ auth_primitives: ["login_required", "permission_required", "user_passes_test", "LoginRequiredMixin", "PermissionRequiredMixin", "permission_classes", "IsAuthenticated", "has_permission"],
56288
+ nonce_primitives: ["csrf_protect", "CsrfViewMiddleware", "@csrf"],
56289
+ entry_point_hooks: ["def ", "@api_view", "path(", "url(", "re_path("],
56290
+ sanitizers: ["escape(", "mark_safe", "html.escape"]
56291
+ },
56292
+ flask: {
56293
+ framework: "flask",
56294
+ auth_primitives: ["login_required", "@login_required", "current_user.is_authenticated", "permission_required"],
56295
+ nonce_primitives: ["CSRFProtect", "csrf_token"],
56296
+ entry_point_hooks: ["@app.route", "@blueprint.route", "@bp.route"],
56297
+ sanitizers: ["escape(", "Markup", "html.escape"]
56298
+ },
56299
+ express: {
56300
+ framework: "express",
56301
+ auth_primitives: ["requireAuth", "authenticate", "passport.authenticate", "req.user", "isAuthenticated", "ensureAuthenticated", "requireLogin", "authorize", "authMiddleware"],
56302
+ nonce_primitives: ["csurf", "csrfProtection", "helmet"],
56303
+ entry_point_hooks: ["app.get", "app.post", "router.get", "router.post"],
56304
+ sanitizers: ["escapeHtml", "encodeURIComponent", "sanitize", "xss"]
56305
+ },
56306
+ spring: {
56307
+ framework: "spring",
56308
+ auth_primitives: ["@PreAuthorize", "@Secured", "@RolesAllowed", "hasRole", "hasAuthority", "isAuthenticated", "SecurityContextHolder"],
56309
+ nonce_primitives: ["csrf", "CsrfToken"],
56310
+ entry_point_hooks: ["@GetMapping", "@PostMapping", "@RequestMapping", "@Controller"],
56311
+ sanitizers: ["HtmlUtils.htmlEscape", "escapeHtml", "StringEscapeUtils"]
56312
+ },
56313
+ symfony: {
56314
+ framework: "symfony",
56315
+ auth_primitives: ["is_granted", "denyAccessUnlessGranted", "->isGranted(", "IsGranted", "Voter"],
56316
+ nonce_primitives: ["isCsrfTokenValid", "csrf_token"],
56317
+ entry_point_hooks: ["#[Route", "@Route", "#[Get", "#[Post"],
56318
+ sanitizers: ["e(", "htmlspecialchars", "escape("]
56319
+ },
56320
+ aspnet: {
56321
+ framework: "aspnet",
56322
+ auth_primitives: ["[Authorize", "[AllowAnonymous", "User.Identity", "IsInRole", "AuthorizeAsync", "IAuthorizationService"],
56323
+ nonce_primitives: ["ValidateAntiForgeryToken", "Antiforgery"],
56324
+ entry_point_hooks: ["[HttpGet", "[HttpPost", "[Route("],
56325
+ sanitizers: ["HtmlEncoder", "Html.Raw", "UrlEncoder"]
56326
+ },
56327
+ rails: {
56328
+ framework: "rails",
56329
+ auth_primitives: ["authenticate_user!", "authorize", "before_action :authenticate", "before_action :authorize", "current_user"],
56330
+ nonce_primitives: ["protect_from_forgery", "form_authenticity_token"],
56331
+ entry_point_hooks: ["def ", "before_action"],
56332
+ sanitizers: ["h(", "html_escape", "sanitize"]
56333
+ }
56305
56334
  };
56306
56335
  function frameworkKnowledge(framework = "wordpress") {
56307
56336
  return FRAMEWORKS[framework.toLowerCase()] ?? null;
56308
56337
  }
56338
+ function listFrameworks2() {
56339
+ const s = new Set;
56340
+ for (const k of Object.keys(FRAMEWORKS))
56341
+ s.add(FRAMEWORKS[k].framework);
56342
+ return [...s];
56343
+ }
56344
+ function lineNoOf(code, pos) {
56345
+ let n = 1;
56346
+ for (let i = 0;i < pos; i++)
56347
+ if (code.charCodeAt(i) === 10)
56348
+ n++;
56349
+ return n;
56350
+ }
56309
56351
  function functionBody(code, name) {
56310
56352
  const idx = code.search(new RegExp(`function\\s+${name}\\s*\\(`));
56311
56353
  if (idx === -1)
@@ -56325,16 +56367,8 @@ function functionBody(code, name) {
56325
56367
  }
56326
56368
  return code.slice(open);
56327
56369
  }
56328
- function lineNoOf(code, pos) {
56329
- let n = 1;
56330
- for (let i = 0;i < pos; i++)
56331
- if (code.charCodeAt(i) === 10)
56332
- n++;
56333
- return n;
56334
- }
56335
56370
  var INDIRECT_AUTH = /\b[a-zA-Z0-9_]*(auth|permission|cap|nonce|can_|check|verify|allow)[a-zA-Z0-9_]*\s*\(/;
56336
- function detectMissingAuthz(code, file) {
56337
- const out = [];
56371
+ function detectWpAuthz(code, file, out) {
56338
56372
  const ajaxRe = /add_action\s*\(\s*['"](wp_ajax(?:_nopriv)?_\w+)['"]\s*,\s*['"](\w+)['"]/g;
56339
56373
  let m;
56340
56374
  while ((m = ajaxRe.exec(code)) !== null) {
@@ -56348,26 +56382,10 @@ function detectMissingAuthz(code, file) {
56348
56382
  const hasNonce = /wp_verify_nonce|check_ajax_referer|check_admin_referer|verify_nonce/.test(body);
56349
56383
  const line = lineNoOf(code, m.index);
56350
56384
  if (nopriv && !hasCap) {
56351
- out.push({
56352
- file,
56353
- line,
56354
- type: "missing_authz",
56355
- category: "Missing capability check (unauthenticated AJAX handler)",
56356
- severity: "high",
56357
- evidence: `add_action('${hook}', '${handler}')`,
56358
- detail: `Unauthenticated AJAX handler '${handler}()' performs no current_user_can()/authorization check — any unauthenticated visitor can trigger it (auth bypass / privilege escalation).`
56359
- });
56385
+ out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated AJAX handler)", severity: "high", evidence: `add_action('${hook}', '${handler}')`, detail: `Unauthenticated AJAX handler '${handler}()' performs no current_user_can()/authorization check — any unauthenticated visitor can trigger it (auth bypass / privilege escalation).` });
56360
56386
  }
56361
56387
  if (!hasNonce) {
56362
- out.push({
56363
- file,
56364
- line,
56365
- type: "missing_nonce",
56366
- category: "Missing nonce verification",
56367
- severity: "medium",
56368
- evidence: `add_action('${hook}', '${handler}')`,
56369
- detail: `AJAX handler '${handler}()' does not verify a nonce (wp_verify_nonce / check_ajax_referer) — CSRF: an attacker can force an authenticated admin to trigger it.`
56370
- });
56388
+ out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', '${handler}')`, detail: `AJAX handler '${handler}()' does not verify a nonce (wp_verify_nonce / check_ajax_referer) — CSRF.` });
56371
56389
  }
56372
56390
  }
56373
56391
  const adminRe = /add_action\s*\(\s*['"](admin_post(?:_nopriv)?_\w+)['"]\s*,\s*['"](\w+)['"]/g;
@@ -56382,26 +56400,10 @@ function detectMissingAuthz(code, file) {
56382
56400
  const hasNonce = /wp_verify_nonce|check_admin_referer|verify_nonce/.test(body);
56383
56401
  const line = lineNoOf(code, m.index);
56384
56402
  if (nopriv && !hasCap) {
56385
- out.push({
56386
- file,
56387
- line,
56388
- type: "missing_authz",
56389
- category: "Missing capability check (unauthenticated admin-post handler)",
56390
- severity: "high",
56391
- evidence: `add_action('${hook}', '${handler}')`,
56392
- detail: `Unauthenticated admin-post handler '${handler}()' has no authorization check — auth bypass / privilege escalation.`
56393
- });
56403
+ out.push({ file, line, type: "missing_authz", category: "Missing capability check (unauthenticated admin-post handler)", severity: "high", evidence: `add_action('${hook}', '${handler}')`, detail: `Unauthenticated admin-post handler '${handler}()' has no authorization check — auth bypass / privilege escalation.` });
56394
56404
  }
56395
56405
  if (!hasNonce) {
56396
- out.push({
56397
- file,
56398
- line,
56399
- type: "missing_nonce",
56400
- category: "Missing nonce verification",
56401
- severity: "medium",
56402
- evidence: `add_action('${hook}', '${handler}')`,
56403
- detail: `admin-post handler '${handler}()' does not verify a nonce — CSRF.`
56404
- });
56406
+ out.push({ file, line, type: "missing_nonce", category: "Missing nonce verification", severity: "medium", evidence: `add_action('${hook}', '${handler}')`, detail: `admin-post handler '${handler}()' does not verify a nonce — CSRF.` });
56405
56407
  }
56406
56408
  }
56407
56409
  const restRe = /register_rest_route\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
@@ -56410,16 +56412,47 @@ function detectMissingAuthz(code, file) {
56410
56412
  const window2 = code.slice(m.index, m.index + 800);
56411
56413
  if (/permission_callback/.test(window2))
56412
56414
  continue;
56413
- out.push({
56414
- file,
56415
- line,
56416
- type: "missing_authz",
56417
- category: "REST route missing permission_callback",
56418
- severity: "high",
56419
- evidence: `register_rest_route('${m[1]}', '${m[2]}')`,
56420
- detail: `REST route '${m[1]}${m[2]}' has no permission_callback — the endpoint defaults to public (auth bypass / unauthenticated data access or mutation).`
56421
- });
56415
+ out.push({ file, line, type: "missing_authz", category: "REST route missing permission_callback", severity: "high", evidence: `register_rest_route('${m[1]}', '${m[2]}')`, detail: `REST route '${m[1]}${m[2]}' has no permission_callback — the endpoint defaults to public (auth bypass).` });
56416
+ }
56417
+ }
56418
+ var ROUTE_DETECTORS = [
56419
+ { framework: "laravel", entry: /Route::(?:get|post|put|patch|delete|any|match|resource)\s*\(\s*['"][^'"]+['"]/g, auth: /->middleware\s*\(\s*['"]auth|middleware\s*\(\s*\[[^\]]*['"]auth|->can\s*\(|\$this->authorize|Gate::|authorize\s*\(|@can\b/, window: 2 },
56420
+ { framework: "django", entry: /@api_view\s*\(|def\s+\w+\s*\(\s*request\s*[,)]/g, auth: /login_required|permission_required|user_passes_test|LoginRequiredMixin|PermissionRequiredMixin|permission_classes|IsAuthenticated|has_permission/, window: 3 },
56421
+ { framework: "flask", entry: /@(?:app|bp|blueprint)\.route\s*\(/g, auth: /login_required|current_user\.is_authenticated|permission_required/, window: 3 },
56422
+ { framework: "express", entry: /(?:app|router)\.(?:get|post|put|delete|patch|use)\s*\(\s*['"][^'"]+['"]/g, auth: /requireAuth|authenticate|passport\.authenticate|req\.user|isAuthenticated|ensureAuthenticated|requireLogin|\.authorize|authMiddleware/, window: 2 },
56423
+ { framework: "spring", entry: /@(?:Get|Post|Put|Delete|Patch|Request)Mapping\s*\(/g, auth: /@PreAuthorize|@Secured|@RolesAllowed|hasRole|hasAuthority|isAuthenticated|SecurityContextHolder/, window: 2 },
56424
+ { framework: "symfony", entry: /#\[(?:Route|Get|Post|Put|Delete|Patch)\b|@Route\s*\(/g, auth: /is_granted|denyAccessUnlessGranted|->isGranted\s*\(|IsGranted|Voter/, window: 2 },
56425
+ { framework: "aspnet", entry: /\[Http(?:Get|Post|Put|Delete|Patch)\b/g, auth: /\[Authorize|User\.Identity|IsInRole|AuthorizeAsync|\[AllowAnonymous|IAuthorizationService/, window: 2 },
56426
+ { framework: "rails", entry: /class\s+\w+\s*<\s*ApplicationController/g, auth: /before_action\s+:authenticate|before_action\s+:authorize|authenticate_user!|authorize\s+|current_user|authenticate/, window: 6 }
56427
+ ];
56428
+ function detectRouteAuthz(code, file, out) {
56429
+ const ls = code.split(`
56430
+ `);
56431
+ for (const d of ROUTE_DETECTORS) {
56432
+ d.entry.lastIndex = 0;
56433
+ let m;
56434
+ while ((m = d.entry.exec(code)) !== null) {
56435
+ const ln = lineNoOf(code, m.index);
56436
+ const window2 = ls.slice(Math.max(0, ln - d.window), Math.min(ls.length, ln + d.window + 1)).join(`
56437
+ `);
56438
+ if (d.auth.test(window2))
56439
+ continue;
56440
+ out.push({
56441
+ file,
56442
+ line: ln,
56443
+ type: "missing_authz",
56444
+ category: `${d.framework}: endpoint without authorization`,
56445
+ severity: "high",
56446
+ evidence: (ls[ln - 1] ?? "").trim(),
56447
+ detail: `${d.framework} endpoint has no authorization enforcement in the surrounding ${d.window}-line window — auth bypass / privilege escalation.`
56448
+ });
56449
+ }
56422
56450
  }
56451
+ }
56452
+ function detectMissingAuthz(code, file) {
56453
+ const out = [];
56454
+ detectWpAuthz(code, file, out);
56455
+ detectRouteAuthz(code, file, out);
56423
56456
  const seen = new Set;
56424
56457
  return out.filter((f) => {
56425
56458
  const key = `${f.type}:${f.line}`;
@@ -56903,6 +56936,8 @@ function runBenchmark() {
56903
56936
  const detectionRate = tp + fn > 0 ? tp / (tp + fn) : 0;
56904
56937
  const falsePositiveRate = fp + tn > 0 ? fp / (fp + tn) : 0;
56905
56938
  const precision = tp + fp > 0 ? tp / (tp + fp) : 0;
56939
+ const recall = detectionRate;
56940
+ const f1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
56906
56941
  const byDetector = {};
56907
56942
  for (const r of results) {
56908
56943
  const d = r.entry.detector ?? "taint";
@@ -56927,6 +56962,8 @@ function runBenchmark() {
56927
56962
  detection_rate: Number(detectionRate.toFixed(3)),
56928
56963
  false_positive_rate: Number(falsePositiveRate.toFixed(3)),
56929
56964
  precision: Number(precision.toFixed(3)),
56965
+ recall: Number(recall.toFixed(3)),
56966
+ f1: Number(f1.toFixed(3)),
56930
56967
  by_detector: detectorMetrics,
56931
56968
  captured_false_positives: {
56932
56969
  total: captured.length,
@@ -58076,14 +58113,17 @@ ${statusLine}` : statusLine },
58076
58113
  return { content: [{ type: "text", text: JSON.stringify(analyzeSecurityState({ sanitizers: arr, sinkType: sink_type })) }] };
58077
58114
  });
58078
58115
  server.registerTool("framework_knowledge", {
58079
- title: "Framework security knowledge graph",
58080
- description: "FRAMEWORK-KNOWLEDGE: return a framework's security primitives as data — authorization primitives (current_user_can, is_user_logged_in), nonce/CSRF primitives (wp_verify_nonce, check_ajax_referer), entry-point hooks (wp_ajax_*, admin_post_*, register_rest_route), and sanitizers. Powers the missing-authz/nonce detector. Extensible to other frameworks.",
58116
+ title: "Framework security knowledge graph (multi-framework)",
58117
+ description: "FRAMEWORK-KNOWLEDGE: return a framework's security primitives as data — authorization primitives, nonce/CSRF primitives, entry-point hooks, and sanitizers. Supports wordpress, laravel, django, flask, express, spring, symfony, aspnet, rails. Powers the multi-framework missing-authz/nonce detector.",
58081
58118
  inputSchema: {
58082
- framework: string2().optional().describe("Framework id (default 'wordpress')")
58119
+ framework: string2().optional().describe("Framework id (wordpress, laravel, django, flask, express, spring, symfony, aspnet, rails)")
58083
58120
  }
58084
58121
  }, async ({ framework }) => {
58085
58122
  const k = frameworkKnowledge(framework ?? "wordpress");
58086
- return { content: [{ type: "text", text: JSON.stringify(k ?? { error: "framework not in the knowledge graph", framework }) }] };
58123
+ if (!k) {
58124
+ return { content: [{ type: "text", text: JSON.stringify({ error: "framework not in the knowledge graph", framework, available: listFrameworks2() }) }] };
58125
+ }
58126
+ return { content: [{ type: "text", text: JSON.stringify(k) }] };
58087
58127
  });
58088
58128
  server.registerTool("variant_scan", {
58089
58129
  title: "Mass variant-mining scan of an install base",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.35",
3
+ "version": "2.4.37",
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": {