blitzstrike 2.4.34 → 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.
- package/CHANGELOG.md +23 -0
- package/dist/index.js +242 -118
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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
|
+
|
|
19
|
+
## [2.4.35] — 2026-09-14
|
|
20
|
+
|
|
21
|
+
### Added — Variant Mining (confirm once, weaponize everywhere)
|
|
22
|
+
|
|
23
|
+
- New `src/variant-scan.ts`: mass-scan an install base (plugin/theme/repo tree)
|
|
24
|
+
with ALL detectors (taint, complex-bugs incl. wrong-context sanitization +
|
|
25
|
+
missing authz/nonce, route-confusion) and report hits grouped by a DERIVED
|
|
26
|
+
signature (`<detector>:<type>`).
|
|
27
|
+
- New `variant_scan` tool: pass `signature` to mine for ONE bug family — the
|
|
28
|
+
pattern that turns one confirmed CVE into hundreds of variants across 10k+
|
|
29
|
+
files.
|
|
30
|
+
|
|
8
31
|
## [2.4.34] — 2026-09-14
|
|
9
32
|
|
|
10
33
|
### Added — Framework Security Knowledge Graph (WordPress authz/nonce)
|
package/dist/index.js
CHANGED
|
@@ -15493,7 +15493,7 @@ async function checkMcpServers() {
|
|
|
15493
15493
|
var init_mcp_status = () => {};
|
|
15494
15494
|
|
|
15495
15495
|
// src/sync.ts
|
|
15496
|
-
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as
|
|
15496
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync21, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
|
|
15497
15497
|
import { join as join19 } from "node:path";
|
|
15498
15498
|
import { homedir as homedir10 } from "node:os";
|
|
15499
15499
|
import { fileURLToPath as fileURLToPath9 } from "node:url";
|
|
@@ -15589,7 +15589,7 @@ function manualCopy(src, dst) {
|
|
|
15589
15589
|
if (e.isDirectory())
|
|
15590
15590
|
manualCopy(s, d);
|
|
15591
15591
|
else
|
|
15592
|
-
writeFileSync8(d,
|
|
15592
|
+
writeFileSync8(d, readFileSync21(s));
|
|
15593
15593
|
}
|
|
15594
15594
|
}
|
|
15595
15595
|
var ROOT7, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
|
|
@@ -15640,7 +15640,7 @@ var init_update = __esm(() => {
|
|
|
15640
15640
|
});
|
|
15641
15641
|
|
|
15642
15642
|
// src/release.ts
|
|
15643
|
-
import { readFileSync as
|
|
15643
|
+
import { readFileSync as readFileSync23, existsSync as existsSync12 } from "node:fs";
|
|
15644
15644
|
import { join as join20 } from "node:path";
|
|
15645
15645
|
import { fileURLToPath as fileURLToPath10 } from "node:url";
|
|
15646
15646
|
import { execSync as execSync2 } from "node:child_process";
|
|
@@ -15648,7 +15648,7 @@ function validateRelease() {
|
|
|
15648
15648
|
const checks = [];
|
|
15649
15649
|
checks.push({ name: "package.json version", ok: true, detail: VERSION });
|
|
15650
15650
|
try {
|
|
15651
|
-
const changelog =
|
|
15651
|
+
const changelog = readFileSync23(join20(ROOT8, "CHANGELOG.md"), "utf8");
|
|
15652
15652
|
const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
|
|
15653
15653
|
const changelogVersion = m?.[1] ?? null;
|
|
15654
15654
|
checks.push({
|
|
@@ -15689,13 +15689,13 @@ var init_release = __esm(() => {
|
|
|
15689
15689
|
});
|
|
15690
15690
|
|
|
15691
15691
|
// src/dependency.ts
|
|
15692
|
-
import { readFileSync as
|
|
15692
|
+
import { readFileSync as readFileSync24 } from "node:fs";
|
|
15693
15693
|
import { join as join21 } from "node:path";
|
|
15694
15694
|
import { fileURLToPath as fileURLToPath11 } from "node:url";
|
|
15695
15695
|
function checkDependencies() {
|
|
15696
15696
|
let pkg = {};
|
|
15697
15697
|
try {
|
|
15698
|
-
pkg = JSON.parse(
|
|
15698
|
+
pkg = JSON.parse(readFileSync24(join21(ROOT9, "package.json"), "utf8"));
|
|
15699
15699
|
} catch {
|
|
15700
15700
|
return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
|
|
15701
15701
|
}
|
|
@@ -15722,7 +15722,7 @@ var init_dependency = __esm(() => {
|
|
|
15722
15722
|
});
|
|
15723
15723
|
|
|
15724
15724
|
// src/cli.ts
|
|
15725
|
-
import { readFileSync as
|
|
15725
|
+
import { readFileSync as readFileSync25, existsSync as existsSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10 } from "node:fs";
|
|
15726
15726
|
import { join as join22, dirname as dirname2 } from "node:path";
|
|
15727
15727
|
import { homedir as homedir11 } from "node:os";
|
|
15728
15728
|
import { fileURLToPath as fileURLToPath12 } from "node:url";
|
|
@@ -15833,7 +15833,7 @@ function resolveCommand() {
|
|
|
15833
15833
|
}
|
|
15834
15834
|
function readJson(p) {
|
|
15835
15835
|
try {
|
|
15836
|
-
return JSON.parse(
|
|
15836
|
+
return JSON.parse(readFileSync25(p, "utf8"));
|
|
15837
15837
|
} catch {
|
|
15838
15838
|
return null;
|
|
15839
15839
|
}
|
|
@@ -15876,7 +15876,7 @@ function copyOpenCodeAgents() {
|
|
|
15876
15876
|
const src = join22(srcDir, f);
|
|
15877
15877
|
const dst = join22(dstDir, f);
|
|
15878
15878
|
if (existsSync13(src)) {
|
|
15879
|
-
writeFileSync9(dst,
|
|
15879
|
+
writeFileSync9(dst, readFileSync25(src, "utf8"));
|
|
15880
15880
|
}
|
|
15881
15881
|
}
|
|
15882
15882
|
}
|
|
@@ -15885,7 +15885,7 @@ function codexWrite(p) {
|
|
|
15885
15885
|
const dir = dirname2(p);
|
|
15886
15886
|
if (!existsSync13(dir))
|
|
15887
15887
|
mkdirSync10(dir, { recursive: true });
|
|
15888
|
-
let existing = existsSync13(p) ?
|
|
15888
|
+
let existing = existsSync13(p) ? readFileSync25(p, "utf8") : "";
|
|
15889
15889
|
if (!existing.trimEnd().endsWith(`
|
|
15890
15890
|
`))
|
|
15891
15891
|
existing += `
|
|
@@ -15904,7 +15904,7 @@ function hermesWrite(p) {
|
|
|
15904
15904
|
const dir = dirname2(p);
|
|
15905
15905
|
if (!existsSync13(dir))
|
|
15906
15906
|
mkdirSync10(dir, { recursive: true });
|
|
15907
|
-
let existing = existsSync13(p) ?
|
|
15907
|
+
let existing = existsSync13(p) ? readFileSync25(p, "utf8") : "";
|
|
15908
15908
|
existing = existing.replace(/^ blitzstrike:\n(?: .*\n?)*/m, "");
|
|
15909
15909
|
if (!existing.trimEnd().endsWith(`
|
|
15910
15910
|
`))
|
|
@@ -30754,7 +30754,7 @@ class StdioServerTransport {
|
|
|
30754
30754
|
|
|
30755
30755
|
// src/server.ts
|
|
30756
30756
|
init_scanner();
|
|
30757
|
-
import { readFileSync as
|
|
30757
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
30758
30758
|
|
|
30759
30759
|
// src/recon.ts
|
|
30760
30760
|
var TIMEOUT_MS = 25000;
|
|
@@ -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:
|
|
56304
|
-
|
|
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
|
|
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
|
-
|
|
56415
|
-
|
|
56416
|
-
|
|
56417
|
-
|
|
56418
|
-
|
|
56419
|
-
|
|
56420
|
-
|
|
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}`;
|
|
@@ -57054,8 +57087,85 @@ function openCount() {
|
|
|
57054
57087
|
return loadObligations().filter((o) => o.status === "open").length;
|
|
57055
57088
|
}
|
|
57056
57089
|
|
|
57090
|
+
// src/variant-scan.ts
|
|
57091
|
+
init_scanner();
|
|
57092
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
57093
|
+
function deriveSignature(detector, type) {
|
|
57094
|
+
return `${detector}:${type}`;
|
|
57095
|
+
}
|
|
57096
|
+
function variantScan(root, opts = {}) {
|
|
57097
|
+
const files = iterSourceFiles(root, opts.maxFiles ?? 5000);
|
|
57098
|
+
const hits = [];
|
|
57099
|
+
for (const file of files) {
|
|
57100
|
+
let code;
|
|
57101
|
+
try {
|
|
57102
|
+
code = readFileSync19(file, "utf8");
|
|
57103
|
+
} catch {
|
|
57104
|
+
continue;
|
|
57105
|
+
}
|
|
57106
|
+
try {
|
|
57107
|
+
const taint = analyzeTaint(file);
|
|
57108
|
+
for (const f of taint.findings) {
|
|
57109
|
+
const type = f.category ?? f.sink ?? "taint";
|
|
57110
|
+
hits.push({
|
|
57111
|
+
file,
|
|
57112
|
+
detector: "taint",
|
|
57113
|
+
type,
|
|
57114
|
+
signature: deriveSignature("taint", type),
|
|
57115
|
+
line: f.sink_line ?? 0,
|
|
57116
|
+
severity: "high",
|
|
57117
|
+
evidence: f.sink ?? ""
|
|
57118
|
+
});
|
|
57119
|
+
}
|
|
57120
|
+
} catch {}
|
|
57121
|
+
for (const f of detectComplexBugs(code, file)) {
|
|
57122
|
+
hits.push({
|
|
57123
|
+
file,
|
|
57124
|
+
detector: "complex_bugs",
|
|
57125
|
+
type: f.type,
|
|
57126
|
+
signature: deriveSignature("complex_bugs", f.type),
|
|
57127
|
+
line: f.line,
|
|
57128
|
+
severity: f.severity,
|
|
57129
|
+
evidence: f.evidence
|
|
57130
|
+
});
|
|
57131
|
+
}
|
|
57132
|
+
for (const f of detectRouteConfusion(code, file)) {
|
|
57133
|
+
hits.push({
|
|
57134
|
+
file,
|
|
57135
|
+
detector: "route_confusion",
|
|
57136
|
+
type: f.type,
|
|
57137
|
+
signature: deriveSignature("route_confusion", f.type),
|
|
57138
|
+
line: f.line,
|
|
57139
|
+
severity: f.severity,
|
|
57140
|
+
evidence: f.evidence
|
|
57141
|
+
});
|
|
57142
|
+
}
|
|
57143
|
+
}
|
|
57144
|
+
const bySignature = {};
|
|
57145
|
+
for (const h of hits)
|
|
57146
|
+
bySignature[h.signature] = (bySignature[h.signature] ?? 0) + 1;
|
|
57147
|
+
const filtered = opts.signature ? hits.filter((h) => h.signature === opts.signature) : hits;
|
|
57148
|
+
const filesWithFindings = new Set(filtered.map((h) => h.file));
|
|
57149
|
+
return {
|
|
57150
|
+
root,
|
|
57151
|
+
files_scanned: files.length,
|
|
57152
|
+
files_with_findings: filesWithFindings.size,
|
|
57153
|
+
total_findings: filtered.length,
|
|
57154
|
+
signature: opts.signature ?? null,
|
|
57155
|
+
by_signature: bySignature,
|
|
57156
|
+
hits: filtered.map((h) => ({
|
|
57157
|
+
file: h.file,
|
|
57158
|
+
detector: h.detector,
|
|
57159
|
+
type: h.type,
|
|
57160
|
+
signature: h.signature,
|
|
57161
|
+
line: h.line,
|
|
57162
|
+
severity: h.severity
|
|
57163
|
+
}))
|
|
57164
|
+
};
|
|
57165
|
+
}
|
|
57166
|
+
|
|
57057
57167
|
// src/enterprise-benchmark.ts
|
|
57058
|
-
import { existsSync as existsSync11, readFileSync as
|
|
57168
|
+
import { existsSync as existsSync11, readFileSync as readFileSync20, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
57059
57169
|
import { join as join18, resolve } from "node:path";
|
|
57060
57170
|
var ROOT6 = resolve(process.cwd(), "bench", "enterprise");
|
|
57061
57171
|
var ENTERPRISE_FIXTURES = [
|
|
@@ -57097,7 +57207,7 @@ function detectFixture(dir) {
|
|
|
57097
57207
|
for (const f of sourceFiles(dir)) {
|
|
57098
57208
|
let code;
|
|
57099
57209
|
try {
|
|
57100
|
-
code =
|
|
57210
|
+
code = readFileSync20(f, "utf8");
|
|
57101
57211
|
} catch {
|
|
57102
57212
|
continue;
|
|
57103
57213
|
}
|
|
@@ -57999,14 +58109,28 @@ ${statusLine}` : statusLine },
|
|
|
57999
58109
|
return { content: [{ type: "text", text: JSON.stringify(analyzeSecurityState({ sanitizers: arr, sinkType: sink_type })) }] };
|
|
58000
58110
|
});
|
|
58001
58111
|
server.registerTool("framework_knowledge", {
|
|
58002
|
-
title: "Framework security knowledge graph",
|
|
58003
|
-
description: "FRAMEWORK-KNOWLEDGE: return a framework's security primitives as data — authorization primitives
|
|
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.",
|
|
58004
58114
|
inputSchema: {
|
|
58005
|
-
framework: string2().optional().describe("Framework id (
|
|
58115
|
+
framework: string2().optional().describe("Framework id (wordpress, laravel, django, flask, express, spring, symfony, aspnet, rails)")
|
|
58006
58116
|
}
|
|
58007
58117
|
}, async ({ framework }) => {
|
|
58008
58118
|
const k = frameworkKnowledge(framework ?? "wordpress");
|
|
58009
|
-
|
|
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) }] };
|
|
58123
|
+
});
|
|
58124
|
+
server.registerTool("variant_scan", {
|
|
58125
|
+
title: "Mass variant-mining scan of an install base",
|
|
58126
|
+
description: "VARIANT-MINING: mass-scan a source tree (plugin/theme/repo install base) with ALL detectors (taint, complex-bugs incl. wrong-context sanitization + missing authz/nonce, route-confusion) and report hits grouped by DERIVED signature (<detector>:<type>). Pass `signature` to mine for ONE bug family (e.g. complex_bugs:missing_authz) — the 'confirm once, weaponize everywhere' pattern that turns one CVE into hundreds of variants across an install base.",
|
|
58127
|
+
inputSchema: {
|
|
58128
|
+
root: string2().describe("Root directory to scan"),
|
|
58129
|
+
max_files: number2().optional().describe("Max files to scan (default 5000)"),
|
|
58130
|
+
signature: string2().optional().describe("Filter to one bug family (e.g. complex_bugs:missing_authz)")
|
|
58131
|
+
}
|
|
58132
|
+
}, async ({ root, max_files, signature }) => {
|
|
58133
|
+
return { content: [{ type: "text", text: JSON.stringify(variantScan(root, { maxFiles: max_files, signature })) }] };
|
|
58010
58134
|
});
|
|
58011
58135
|
server.registerTool("payload_lookup", {
|
|
58012
58136
|
title: "Look up exploit payloads",
|
|
@@ -58546,7 +58670,7 @@ ${statusLine}` : statusLine },
|
|
|
58546
58670
|
description: "Route confusion (separate class from source→sink taint): detect attacker-controlled dispatch (call_user_func/forward/dispatch), dynamic method calls ($obj->$m()), dynamic include (LFI), batch/proxy forwarding without per-route auth re-check, and route matching without path normalization.",
|
|
58547
58671
|
inputSchema: { path: string2().describe("Source file path (PHP)") }
|
|
58548
58672
|
}, async ({ path }) => {
|
|
58549
|
-
const code =
|
|
58673
|
+
const code = readFileSync22(path, "utf8");
|
|
58550
58674
|
const findings = detectRouteConfusion(code, path);
|
|
58551
58675
|
return {
|
|
58552
58676
|
content: [{
|
|
@@ -58560,7 +58684,7 @@ ${statusLine}` : statusLine },
|
|
|
58560
58684
|
description: "Complex-bug detection beyond taint: deserialization→POP gadget chain (unserialize + magic method), type juggling (loose ==/!= vs hash/secret), mass assignment (extract/parse_str without a safe flag), prototype pollution (unsafe merge of request data), CRLF/header injection (user input into a header), path confusion (user input into a file path without canonicalization), SSRF (user-controlled URL with no host allowlist), XXE (user XML parsed without disabling entities), SSTI (user input into a template render with no sandbox), wrong-context sanitization (sanitize_text_field() into a SQL query), and missing authorization/nonce on WordPress entry points (wp_ajax_* / admin_post_* / register_rest_route without current_user_can, nonce verification, or permission_callback).",
|
|
58561
58685
|
inputSchema: { path: string2().describe("Source file path") }
|
|
58562
58686
|
}, async ({ path }) => {
|
|
58563
|
-
const code =
|
|
58687
|
+
const code = readFileSync22(path, "utf8");
|
|
58564
58688
|
const findings = detectComplexBugs(code, path);
|
|
58565
58689
|
return {
|
|
58566
58690
|
content: [{
|
|
@@ -58627,7 +58751,7 @@ ${statusLine}` : statusLine },
|
|
|
58627
58751
|
let lang = language;
|
|
58628
58752
|
if (path && !code) {
|
|
58629
58753
|
try {
|
|
58630
|
-
src =
|
|
58754
|
+
src = readFileSync22(path, "utf8");
|
|
58631
58755
|
} catch {
|
|
58632
58756
|
return { content: [{ type: "text", text: JSON.stringify({ error: `cannot read ${path}` }) }] };
|
|
58633
58757
|
}
|
|
@@ -58868,7 +58992,7 @@ async function serve() {
|
|
|
58868
58992
|
|
|
58869
58993
|
// src/index.ts
|
|
58870
58994
|
init_cli();
|
|
58871
|
-
import { readFileSync as
|
|
58995
|
+
import { readFileSync as readFileSync26, existsSync as existsSync14 } from "node:fs";
|
|
58872
58996
|
import { join as join23 } from "node:path";
|
|
58873
58997
|
import { fileURLToPath as fileURLToPath13 } from "node:url";
|
|
58874
58998
|
var ROOT11 = join23(fileURLToPath13(new URL(".", import.meta.url)), "..");
|
|
@@ -58876,7 +59000,7 @@ function readVersion() {
|
|
|
58876
59000
|
try {
|
|
58877
59001
|
const p = join23(ROOT11, "package.json");
|
|
58878
59002
|
if (existsSync14(p))
|
|
58879
|
-
return JSON.parse(
|
|
59003
|
+
return JSON.parse(readFileSync26(p, "utf8")).version ?? "1.0.0";
|
|
58880
59004
|
} catch {}
|
|
58881
59005
|
return "1.0.0";
|
|
58882
59006
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzstrike",
|
|
3
|
-
"version": "2.4.
|
|
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": {
|