@vibefoundry/vibe-shield 1.1.0 → 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.
Files changed (39) hide show
  1. package/README.md +74 -48
  2. package/SECURITY.md +1 -1
  3. package/SKILL.md +27 -94
  4. package/dist/bin/vibe-shield.d.ts +1 -1
  5. package/dist/bin/vibe-shield.js +572 -541
  6. package/dist/src/init/scaffold.d.ts +29 -31
  7. package/dist/src/init/scaffold.js +69 -153
  8. package/dist/src/mcp/server.js +6 -2
  9. package/dist/src/mcp-guard/skill-auditor.js +11 -3
  10. package/dist/src/reporter/console-reporter.js +17 -3
  11. package/dist/src/reporter/executive.d.ts +4 -0
  12. package/dist/src/reporter/executive.js +58 -0
  13. package/dist/src/reporter/formatters.d.ts +1 -1
  14. package/dist/src/reporter/formatters.js +10 -3
  15. package/dist/src/reporter/patch-prompt-writer.js +5 -5
  16. package/dist/src/scanner/attack-surface.d.ts +4 -0
  17. package/dist/src/scanner/attack-surface.js +50 -0
  18. package/dist/src/scanner/engine.d.ts +2 -3
  19. package/dist/src/scanner/engine.js +21 -35
  20. package/dist/src/scanner/mappings.d.ts +17 -0
  21. package/dist/src/scanner/mappings.js +39 -0
  22. package/dist/src/scanner/redact.d.ts +1 -0
  23. package/dist/src/scanner/redact.js +12 -0
  24. package/dist/src/scanner/rules/database-exposure.js +1 -1
  25. package/dist/src/scanner/rules/hardening.d.ts +5 -0
  26. package/dist/src/scanner/rules/hardening.js +116 -0
  27. package/dist/src/scanner/rules/helpers.js +2 -0
  28. package/dist/src/scanner/rules/index.js +6 -0
  29. package/dist/src/scanner/rules/response-hardening.d.ts +2 -0
  30. package/dist/src/scanner/rules/response-hardening.js +120 -0
  31. package/dist/src/scanner/rules/secret-entropy.js +0 -2
  32. package/dist/src/scanner/rules/shared.d.ts +1 -1
  33. package/dist/src/scanner/rules/shared.js +1 -1
  34. package/dist/src/scanner/types.d.ts +33 -1
  35. package/dist/src/scanner/types.js +6 -2
  36. package/dist/src/scanner/version.d.ts +2 -0
  37. package/dist/src/scanner/version.js +69 -0
  38. package/dist/src/secrets/patterns.js +40 -0
  39. package/package.json +99 -96
@@ -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,2 @@
1
+ import type { Rule } from '../types';
2
+ export declare const RESPONSE_HARDENING_RULES: readonly Rule[];
@@ -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')
@@ -81,7 +81,7 @@ export declare const confident: (value: Confidence, boost?: boolean) => Confiden
81
81
  * Single source of truth: `init` writes it into `.mcp.json`, and the auditor
82
82
  * recognises it there. Keeping one constant means the two can never drift.
83
83
  */
84
- export declare const SELF_GITHUB_SPEC = "github:avivgerz-hue/vibe-shield";
84
+ export declare const SELF_GITHUB_SPEC = "@vibefoundry/vibe-shield";
85
85
  /** First-party install specs the auditor vouches for without a `@sha`. */
86
86
  export declare const SELF_INSTALL_SPECS: readonly string[];
87
87
  /**
@@ -137,7 +137,7 @@ exports.confident = confident;
137
137
  * Single source of truth: `init` writes it into `.mcp.json`, and the auditor
138
138
  * recognises it there. Keeping one constant means the two can never drift.
139
139
  */
140
- exports.SELF_GITHUB_SPEC = 'github:avivgerz-hue/vibe-shield';
140
+ exports.SELF_GITHUB_SPEC = '@vibefoundry/vibe-shield';
141
141
  /** First-party install specs the auditor vouches for without a `@sha`. */
142
142
  exports.SELF_INSTALL_SPECS = [exports.SELF_GITHUB_SPEC];
143
143
  /**
@@ -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;
@@ -1,4 +1,6 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRuleCategory = exports.isSeverity = exports.meetsSeverity = exports.severityRank = exports.emptySeverityCounts = exports.emptyProjectContext = exports.LANGUAGE_EXTENSIONS = exports.GRADE_THRESHOLDS = exports.GRADES = exports.SEVERITY_SCORE_CEILING = exports.SEVERITY_SCORE_PENALTY = exports.SEVERITY_WEIGHT = exports.CATEGORY_TITLES = exports.RULE_CATEGORIES = exports.CONFIDENCES = exports.SEVERITIES = void 0;
2
4
  /**
3
5
  * vibe-shield :: scanner type system
4
6
  * ----------------------------------
@@ -7,8 +9,6 @@
7
9
  * `ScanResult` can be cached to disk, emitted as SARIF, or handed to an agent
8
10
  * as a patch prompt without any transformation.
9
11
  */
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.isRuleCategory = exports.isSeverity = exports.meetsSeverity = exports.severityRank = exports.emptySeverityCounts = exports.emptyProjectContext = exports.LANGUAGE_EXTENSIONS = exports.GRADE_THRESHOLDS = exports.GRADES = exports.SEVERITY_SCORE_CEILING = exports.SEVERITY_SCORE_PENALTY = exports.SEVERITY_WEIGHT = exports.CATEGORY_TITLES = exports.RULE_CATEGORIES = exports.CONFIDENCES = exports.SEVERITIES = void 0;
12
12
  /** Ordered list of severities, most severe first. */
13
13
  exports.SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
14
14
  /** Detection confidence reported alongside every finding. */
@@ -23,6 +23,8 @@ exports.RULE_CATEGORIES = [
23
23
  'database-exposure',
24
24
  'ai-vulnerabilities',
25
25
  'secret-entropy',
26
+ 'infrastructure',
27
+ 'dependencies',
26
28
  'agent-integrity',
27
29
  ];
28
30
  exports.CATEGORY_TITLES = {
@@ -31,6 +33,8 @@ exports.CATEGORY_TITLES = {
31
33
  'database-exposure': 'Database exposure (RLS, SQLi, service role)',
32
34
  'ai-vulnerabilities': 'AI / LLM & classic web vulnerabilities',
33
35
  'secret-entropy': 'Hardcoded credentials & high-entropy secrets',
36
+ infrastructure: 'Infrastructure, headers & containers',
37
+ dependencies: 'Supply-chain & dependency hygiene',
34
38
  'agent-integrity': 'Agent skill & MCP poisoning',
35
39
  };
36
40
  /** Relative severity weight used by the scoring model. */
@@ -0,0 +1,2 @@
1
+ /** Package version, resolved once from the nearest package.json. */
2
+ export declare function packageVersion(): Promise<string>;
@@ -0,0 +1,69 @@
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.packageVersion = packageVersion;
37
+ const node_fs_1 = require("node:fs");
38
+ const path = __importStar(require("node:path"));
39
+ const VERSION = process.env.npm_package_version ?? '1.0.0';
40
+ let cachedVersion = null;
41
+ /** Package version, resolved once from the nearest package.json. */
42
+ async function packageVersion() {
43
+ if (cachedVersion)
44
+ return cachedVersion;
45
+ let dir = __dirname;
46
+ for (let depth = 0; depth < 6; depth += 1) {
47
+ try {
48
+ const raw = await node_fs_1.promises.readFile(path.join(dir, 'package.json'), 'utf8');
49
+ const parsed = JSON.parse(raw);
50
+ // Match the scoped name *or* the legacy bare one, so an install made
51
+ // before the package was published under `@vibefoundry/` still resolves
52
+ // its own version instead of reporting the fallback.
53
+ const isThisPackage = parsed.name === '@vibefoundry/vibe-shield' || parsed.name === 'vibe-shield';
54
+ if (isThisPackage && parsed.version) {
55
+ cachedVersion = parsed.version;
56
+ return cachedVersion;
57
+ }
58
+ }
59
+ catch {
60
+ // Keep walking up.
61
+ }
62
+ const parent = path.dirname(dir);
63
+ if (parent === dir)
64
+ break;
65
+ dir = parent;
66
+ }
67
+ cachedVersion = VERSION;
68
+ return cachedVersion;
69
+ }
@@ -234,6 +234,46 @@ exports.SECRET_PATTERNS = [
234
234
  minEntropy: 3,
235
235
  note: 'Datadog API/application key.',
236
236
  },
237
+ {
238
+ id: 'gitlab-token', vendor: 'GitLab', family: 'saas', severity: 'high',
239
+ regex: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, minEntropy: 3,
240
+ note: 'GitLab personal access token.',
241
+ },
242
+ {
243
+ id: 'digitalocean-token', vendor: 'DigitalOcean', family: 'saas', severity: 'high',
244
+ regex: /\bdop_v1_[a-f0-9]{64}\b/g, minEntropy: 3,
245
+ note: 'DigitalOcean API token.',
246
+ },
247
+ {
248
+ id: 'linear-token', vendor: 'Linear', family: 'saas', severity: 'high',
249
+ regex: /\blin_api_[A-Za-z0-9]{40}\b/g, minEntropy: 3,
250
+ note: 'Linear API token.',
251
+ },
252
+ {
253
+ id: 'cloudflare-token', vendor: 'Cloudflare', family: 'saas', severity: 'high',
254
+ regex: /\bCLOUDFLARE_API_TOKEN\b["']?\s*[:=]\s*["']([A-Za-z0-9_-]{40})["']/gi, minEntropy: 3,
255
+ note: 'A value assigned to a Cloudflare API token variable.',
256
+ },
257
+ {
258
+ id: 'cohere-token', vendor: 'Cohere', family: 'saas', severity: 'high',
259
+ regex: /\bCOHERE_API_KEY\b["']?\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi, minEntropy: 3,
260
+ note: 'A value assigned to a Cohere API key variable.',
261
+ },
262
+ {
263
+ id: 'mistral-token', vendor: 'Mistral', family: 'saas', severity: 'high',
264
+ regex: /\bMISTRAL_API_KEY\b["']?\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi, minEntropy: 3,
265
+ note: 'A value assigned to a Mistral API key variable.',
266
+ },
267
+ {
268
+ id: 'groq-token', vendor: 'Groq', family: 'saas', severity: 'high',
269
+ regex: /\bgsk_[A-Za-z0-9]{30,}\b/g, minEntropy: 3,
270
+ note: 'Groq API key.',
271
+ },
272
+ {
273
+ id: 'together-token', vendor: 'Together AI', family: 'saas', severity: 'high',
274
+ regex: /\bTOGETHER_API_KEY\b["']?\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi, minEntropy: 3,
275
+ note: 'A value assigned to a Together AI API key variable.',
276
+ },
237
277
  {
238
278
  id: 'jwt',
239
279
  vendor: 'JSON Web Token',