blitzstrike 2.4.35 → 2.4.36

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 +11 -0
  2. package/dist/index.js +135 -99
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ 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.36] — 2026-09-14
9
+
10
+ ### Changed — Framework Knowledge Graph is now MULTI-framework (not WordPress-only)
11
+
12
+ - `framework-security.ts` now covers 9 frameworks: WordPress, Laravel, Django,
13
+ Flask, Express, Spring, Symfony, ASP.NET Core, Ruby on Rails — each with its
14
+ auth / nonce / entry-point / sanitizer primitives as data.
15
+ - `detectMissingAuthz` now detects missing authorization across ALL frameworks
16
+ (route middleware, decorators, annotations), not just WP AJAX/REST hooks.
17
+ - `framework_knowledge` tool returns the framework list on an unknown id.
18
+
8
19
  ## [2.4.35] — 2026-09-14
9
20
 
10
21
  ### 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}`;
@@ -58076,14 +58109,17 @@ ${statusLine}` : statusLine },
58076
58109
  return { content: [{ type: "text", text: JSON.stringify(analyzeSecurityState({ sanitizers: arr, sinkType: sink_type })) }] };
58077
58110
  });
58078
58111
  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.",
58112
+ title: "Framework security knowledge graph (multi-framework)",
58113
+ 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
58114
  inputSchema: {
58082
- framework: string2().optional().describe("Framework id (default 'wordpress')")
58115
+ framework: string2().optional().describe("Framework id (wordpress, laravel, django, flask, express, spring, symfony, aspnet, rails)")
58083
58116
  }
58084
58117
  }, async ({ framework }) => {
58085
58118
  const k = frameworkKnowledge(framework ?? "wordpress");
58086
- return { content: [{ type: "text", text: JSON.stringify(k ?? { error: "framework not in the knowledge graph", framework }) }] };
58119
+ if (!k) {
58120
+ return { content: [{ type: "text", text: JSON.stringify({ error: "framework not in the knowledge graph", framework, available: listFrameworks2() }) }] };
58121
+ }
58122
+ return { content: [{ type: "text", text: JSON.stringify(k) }] };
58087
58123
  });
58088
58124
  server.registerTool("variant_scan", {
58089
58125
  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.36",
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": {