@all-safe-projects/allsafe-vibeguard 0.1.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/LICENSE ADDED
@@ -0,0 +1,38 @@
1
+ AllSafe VibeGuard Proprietary License
2
+ Version 1.0, 2026
3
+
4
+ Copyright (c) 2026 All Safe Areas. All rights reserved.
5
+
6
+ Subject to these terms, All Safe Areas grants you a limited, revocable,
7
+ non-exclusive, non-transferable right to download, install, and execute an
8
+ unmodified copy of AllSafe VibeGuard solely for internal security evaluation
9
+ and source-code scanning.
10
+
11
+ You may not, without prior written permission from All Safe Areas:
12
+
13
+ 1. modify, adapt, translate, or create derivative works of the software;
14
+ 2. copy or redistribute the software except for copies strictly necessary for
15
+ the permitted internal use;
16
+ 3. publish, sublicense, sell, rent, lease, or otherwise transfer the software;
17
+ 4. provide the software or its functionality as a hosted, managed, or
18
+ commercial service;
19
+ 5. remove or alter copyright, attribution, or proprietary notices;
20
+ 6. reverse engineer, decompile, or disassemble the software, except only to
21
+ the extent such restriction is prohibited by applicable law; or
22
+ 7. use the software to violate applicable law or the rights of another party.
23
+
24
+ No rights are granted to any source code, trademark, service mark, logo,
25
+ patent, or other intellectual property except the limited execution right
26
+ stated above. All rights not expressly granted are reserved by All Safe Areas.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
30
+ PURPOSE, TITLE, AND NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY LAW,
31
+ ALL SAFE AREAS WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
32
+ CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR FOR LOSS OF DATA, PROFITS, OR BUSINESS,
33
+ ARISING FROM OR RELATED TO THE SOFTWARE OR ITS USE.
34
+
35
+ These terms terminate automatically if you breach them. On termination, you
36
+ must stop using the software and delete all copies in your possession or
37
+ control. Additional rights may be granted only by a separate written agreement
38
+ signed by All Safe Areas.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # AllSafe VibeGuard
2
+
3
+ AllSafe VibeGuard is a local security deployment gate for JavaScript and TypeScript projects generated or modified with AI tools. It scans source files on your machine and reports high-risk patterns before deployment.
4
+
5
+ > Experimental MVP. VibeGuard is a focused static checker, not a replacement for code review, dependency auditing, penetration testing, or a complete SAST platform.
6
+
7
+ ## Quick start
8
+
9
+ Node.js 20 or newer is required.
10
+
11
+ ```bash
12
+ npx @all-safe-projects/allsafe-vibeguard scan .
13
+ ```
14
+
15
+ The scanner reads the selected local directory. It does not upload project source code.
16
+
17
+ ## CLI
18
+
19
+ ```text
20
+ allsafe-vibe scan [path] [options]
21
+ allsafe-vibe [path] [options]
22
+
23
+ Options:
24
+ --json Print JSON
25
+ --fail-on <severity> critical, high, medium, low, none
26
+ --no-color Disable terminal colors
27
+ -h, --help Show help
28
+ -v, --version Show version
29
+ ```
30
+
31
+ Examples:
32
+
33
+ ```bash
34
+ npx @all-safe-projects/allsafe-vibeguard scan . --fail-on medium
35
+ npx @all-safe-projects/allsafe-vibeguard scan ./services/api --json
36
+ ```
37
+
38
+ The default failure threshold is `high`. Exit code `0` means the configured threshold was not reached, `1` means it was reached, and `2` means the scan could not complete.
39
+
40
+ ## Current checks
41
+
42
+ - Embedded private keys and credentials shaped like AWS, GitHub, or Stripe secrets
43
+ - Hardcoded passwords, tokens, API keys, and sensitive `.env` files
44
+ - Predictable JWT secret fallbacks and JWT decoding without signature verification
45
+ - Potential SSRF, command injection, path traversal, and dynamic code execution
46
+ - Disabled TLS certificate verification and wildcard CORS configuration
47
+
48
+ Each finding includes severity, confidence, evidence with common credentials redacted, impact, and a remediation suggestion.
49
+
50
+ ## Verdicts
51
+
52
+ | Verdict | Meaning |
53
+ | --- | --- |
54
+ | `PASS` | No findings were detected by the current rule set. |
55
+ | `REVIEW` | One or more low or medium findings require review. |
56
+ | `BLOCK` | One or more high or critical findings were detected. |
57
+
58
+ ## Programmatic API
59
+
60
+ ```js
61
+ import { scanProject } from "@all-safe-projects/allsafe-vibeguard";
62
+
63
+ const result = await scanProject(".");
64
+ console.log(result.verdict, result.findings);
65
+ ```
66
+
67
+ Optional resource limits can be overridden for controlled environments:
68
+
69
+ ```js
70
+ const result = await scanProject(".", {
71
+ limits: {
72
+ maxFiles: 5_000,
73
+ timeoutMs: 15_000
74
+ }
75
+ });
76
+ ```
77
+
78
+ Default limits protect the scanner from unexpectedly large or hostile directory trees: 10,000 files, 5,000 directories, depth 30, 25 MB total scanned content, 1 MB per file, 1,000 findings, and 30 seconds. Exceeding a limit stops the scan with exit code `2`; it does not silently return an incomplete result.
79
+
80
+ ## Limitations
81
+
82
+ VibeGuard uses focused pattern-based checks. It can produce false positives and false negatives, and a `PASS` result is not proof that an application is secure. Review findings in context and use layered security testing before production deployment.
83
+
84
+ ## Security reports
85
+
86
+ Report vulnerabilities through the [All Safe Areas vulnerability disclosure page](https://allsafeareas.org/security/vulnerability-disclosure).
87
+
88
+ ## License
89
+
90
+ This package is proprietary and distributed as `UNLICENSED`. Limited internal-use rights and restrictions are stated in [LICENSE](./LICENSE).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import process from "node:process";
5
+ import { formatTerminalReport } from "./reporters/terminal.js";
6
+ import { scanProject } from "./scanner.js";
7
+ const SEVERITY_ORDER = {
8
+ critical: 4,
9
+ high: 3,
10
+ medium: 2,
11
+ low: 1
12
+ };
13
+ function readPackageVersion() {
14
+ try {
15
+ const packageJsonUrl = new URL("../package.json", import.meta.url);
16
+ const packageJson = JSON.parse(readFileSync(packageJsonUrl, "utf8"));
17
+ return packageJson.version ?? "0.0.0";
18
+ }
19
+ catch {
20
+ return "0.0.0";
21
+ }
22
+ }
23
+ function printHelp() {
24
+ console.log(`AllSafe VibeGuard ${readPackageVersion()}
25
+
26
+ Usage:
27
+ allsafe-vibe scan [path] [options]
28
+ allsafe-vibe [path] [options]
29
+
30
+ Options:
31
+ --json Print JSON
32
+ --fail-on <severity> critical, high, medium, low, none
33
+ --no-color Disable terminal colors
34
+ -h, --help Show help
35
+ -v, --version Show version
36
+
37
+ Examples:
38
+ allsafe-vibe scan .
39
+ allsafe-vibe scan ./services/api
40
+ allsafe-vibe scan . --json
41
+ allsafe-vibe scan . --fail-on medium
42
+ `);
43
+ }
44
+ function parseFailThreshold(value) {
45
+ if (value === "critical" ||
46
+ value === "high" ||
47
+ value === "medium" ||
48
+ value === "low" ||
49
+ value === "none") {
50
+ return value;
51
+ }
52
+ throw new Error("--fail-on must be critical, high, medium, low, or none");
53
+ }
54
+ function parseArguments(args) {
55
+ if (args.includes("--help") || args.includes("-h")) {
56
+ printHelp();
57
+ return null;
58
+ }
59
+ if (args.includes("--version") ||
60
+ args.includes("-v")) {
61
+ console.log(readPackageVersion());
62
+ return null;
63
+ }
64
+ const remaining = [...args];
65
+ if (remaining[0] === "scan") {
66
+ remaining.shift();
67
+ }
68
+ let target = ".";
69
+ let json = false;
70
+ let color = true;
71
+ let failOn = "high";
72
+ for (let index = 0; index < remaining.length; index += 1) {
73
+ const argument = remaining[index];
74
+ if (!argument) {
75
+ continue;
76
+ }
77
+ if (argument === "--json") {
78
+ json = true;
79
+ continue;
80
+ }
81
+ if (argument === "--no-color") {
82
+ color = false;
83
+ continue;
84
+ }
85
+ if (argument === "--fail-on") {
86
+ failOn = parseFailThreshold(remaining[index + 1]);
87
+ index += 1;
88
+ continue;
89
+ }
90
+ if (argument.startsWith("--fail-on=")) {
91
+ failOn = parseFailThreshold(argument.slice("--fail-on=".length));
92
+ continue;
93
+ }
94
+ if (argument.startsWith("-")) {
95
+ throw new Error(`Unknown option: ${argument}`);
96
+ }
97
+ if (target !== ".") {
98
+ throw new Error(`Unexpected extra path: ${argument}`);
99
+ }
100
+ target = argument;
101
+ }
102
+ return {
103
+ target: resolve(target),
104
+ json,
105
+ color,
106
+ failOn
107
+ };
108
+ }
109
+ function shouldFail(findings, threshold) {
110
+ if (threshold === "none") {
111
+ return false;
112
+ }
113
+ return findings.some((finding) => SEVERITY_ORDER[finding.severity] >=
114
+ SEVERITY_ORDER[threshold]);
115
+ }
116
+ async function main() {
117
+ try {
118
+ const options = parseArguments(process.argv.slice(2));
119
+ if (!options) {
120
+ return;
121
+ }
122
+ const result = await scanProject(options.target);
123
+ if (options.json) {
124
+ console.log(JSON.stringify(result, null, 2));
125
+ }
126
+ else {
127
+ console.log(formatTerminalReport(result, options.color));
128
+ }
129
+ process.exitCode = shouldFail(result.findings, options.failOn)
130
+ ? 1
131
+ : 0;
132
+ }
133
+ catch (error) {
134
+ console.error(`AllSafe VibeGuard error: ${error instanceof Error
135
+ ? error.message
136
+ : String(error)}`);
137
+ process.exitCode = 2;
138
+ }
139
+ }
140
+ void main();
@@ -0,0 +1,2 @@
1
+ export { scanProject } from "./scanner.js";
2
+ export type { Confidence, Finding, FindingSummary, ScanLimits, ScanOptions, ScanResult, Severity, Verdict } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { scanProject } from "./scanner.js";
@@ -0,0 +1,2 @@
1
+ import type { ScanResult } from "../types.js";
2
+ export declare function formatTerminalReport(result: ScanResult, useColor?: boolean): string;
@@ -0,0 +1,60 @@
1
+ import { sanitizeTerminalText } from "../utils/evidence.js";
2
+ const ANSI = {
3
+ reset: "\u001b[0m",
4
+ bold: "\u001b[1m",
5
+ red: "\u001b[31m",
6
+ yellow: "\u001b[33m",
7
+ blue: "\u001b[34m",
8
+ green: "\u001b[32m",
9
+ gray: "\u001b[90m"
10
+ };
11
+ function paint(text, code, enabled) {
12
+ return enabled
13
+ ? `${code}${text}${ANSI.reset}`
14
+ : text;
15
+ }
16
+ function severityColor(severity) {
17
+ switch (severity) {
18
+ case "critical":
19
+ case "high":
20
+ return ANSI.red;
21
+ case "medium":
22
+ return ANSI.yellow;
23
+ case "low":
24
+ return ANSI.blue;
25
+ }
26
+ }
27
+ function formatFinding(finding, useColor) {
28
+ const severity = paint(finding.severity.toUpperCase().padEnd(8), severityColor(finding.severity), useColor);
29
+ const safe = (value) => sanitizeTerminalText(value);
30
+ return [
31
+ `${severity} ${safe(finding.id)} ${safe(finding.title)}`,
32
+ ` ${safe(finding.file)}:${finding.line}`,
33
+ ` Evidence: ${safe(finding.evidence)}`,
34
+ ` Risk: ${safe(finding.impact)}`,
35
+ ` Fix: ${safe(finding.remediation)}`
36
+ ].join("\n");
37
+ }
38
+ export function formatTerminalReport(result, useColor = Boolean(process.stdout.isTTY)) {
39
+ const verdictColor = result.verdict === "PASS"
40
+ ? ANSI.green
41
+ : result.verdict === "REVIEW"
42
+ ? ANSI.yellow
43
+ : ANSI.red;
44
+ const header = [
45
+ paint("AllSafe VibeGuard", ANSI.bold, useColor),
46
+ `Target: ${sanitizeTerminalText(result.root)}`,
47
+ `Verdict: ${paint(result.verdict, verdictColor, useColor)}`,
48
+ `Score: ${result.score}/100`,
49
+ `Files: ${result.scannedFiles}`,
50
+ `Time: ${result.durationMs} ms`,
51
+ `Issues: ${result.summary.critical} critical, ${result.summary.high} high, ${result.summary.medium} medium, ${result.summary.low} low`
52
+ ].join("\n");
53
+ if (result.findings.length === 0) {
54
+ return `${header}\n\n${paint("No findings detected by the current rule set.", ANSI.green, useColor)}`;
55
+ }
56
+ const findings = result.findings
57
+ .map((finding) => formatFinding(finding, useColor))
58
+ .join("\n\n");
59
+ return `${header}\n\n${findings}\n\n${paint("This scan is not proof that the application is secure.", ANSI.gray, useColor)}`;
60
+ }
@@ -0,0 +1,2 @@
1
+ import type { Finding, ProjectFile } from "../types.js";
2
+ export declare function scanDangerousCode(files: ProjectFile[]): Finding[];
@@ -0,0 +1,129 @@
1
+ import { redactEvidence } from "../utils/evidence.js";
2
+ const CODE_PATTERNS = [
3
+ {
4
+ id: "ASA-CODE-001",
5
+ title: "Dynamic code execution with eval",
6
+ severity: "high",
7
+ category: "code-execution",
8
+ confidence: "high",
9
+ regex: /\beval\s*\(/,
10
+ explanation: "The application executes text as JavaScript code.",
11
+ impact: "If an attacker controls the value, this may become arbitrary code execution.",
12
+ remediation: "Remove eval. Parse a strict data format or map operations to predefined functions."
13
+ },
14
+ {
15
+ id: "ASA-CODE-002",
16
+ title: "Dynamic Function constructor",
17
+ severity: "high",
18
+ category: "code-execution",
19
+ confidence: "high",
20
+ regex: /\bnew\s+Function\s*\(/,
21
+ explanation: "The Function constructor compiles a string into executable JavaScript.",
22
+ impact: "Attacker-controlled input may lead to arbitrary code execution.",
23
+ remediation: "Replace dynamic code generation with explicit allowlisted operations."
24
+ },
25
+ {
26
+ id: "ASA-CMD-001",
27
+ title: "Possible command injection",
28
+ severity: "critical",
29
+ category: "command-injection",
30
+ confidence: "medium",
31
+ regex: /\b(?:exec|execSync)\s*\([^\n]*(?:req|request)\.(?:body|query|params)/,
32
+ explanation: "A shell command appears to use data taken directly from an HTTP request.",
33
+ impact: "An attacker may execute operating-system commands with the application's privileges.",
34
+ remediation: "Avoid a shell. Use execFile or spawn with a fixed executable and strict argument allowlists."
35
+ },
36
+ {
37
+ id: "ASA-SSRF-001",
38
+ title: "Possible server-side request forgery",
39
+ severity: "critical",
40
+ category: "ssrf",
41
+ confidence: "medium",
42
+ regex: /\b(?:fetch|request|axios\.(?:get|post|request))\s*\([^\n]*(?:req|request)\.(?:body|query|params)/,
43
+ explanation: "A server-side request appears to use a URL supplied by the client.",
44
+ impact: "An attacker may access localhost, private networks, cloud metadata, or attacker-controlled endpoints.",
45
+ remediation: "Allowlist destinations, validate resolved IPs, block private ranges, restrict redirects, and isolate outbound workers."
46
+ },
47
+ {
48
+ id: "ASA-AUTH-001",
49
+ title: "Predictable JWT secret fallback",
50
+ severity: "critical",
51
+ category: "authentication",
52
+ confidence: "high",
53
+ regex: /process\.env\.[A-Z0-9_]*(?:JWT|SECRET)[A-Z0-9_]*\s*\|\|\s*["'`][^"'`]+["'`]/i,
54
+ explanation: "JWT signing falls back to a hardcoded value when an environment variable is missing.",
55
+ impact: "An attacker who knows the fallback may forge authentication tokens.",
56
+ remediation: "Fail application startup when the secret is absent. Never use a default signing secret."
57
+ },
58
+ {
59
+ id: "ASA-AUTH-002",
60
+ title: "JWT decoded without signature verification",
61
+ severity: "high",
62
+ category: "authentication",
63
+ confidence: "medium",
64
+ regex: /\b(?:jwt|jsonwebtoken)\.decode\s*\(/,
65
+ explanation: "The code decodes a JWT but may not verify its signature and claims.",
66
+ impact: "Untrusted token contents may be accepted as identity or authorization data.",
67
+ remediation: "Verify the signature, issuer, audience, expiry, and allowed algorithms."
68
+ },
69
+ {
70
+ id: "ASA-FILE-001",
71
+ title: "Possible path traversal",
72
+ severity: "high",
73
+ category: "path-traversal",
74
+ confidence: "medium",
75
+ regex: /\b(?:readFile|writeFile|createReadStream|sendFile)\s*\([^\n]*(?:req|request)\.(?:body|query|params)/,
76
+ explanation: "A filesystem path appears to use data supplied directly by the client.",
77
+ impact: "An attacker may read or overwrite files outside the intended directory.",
78
+ remediation: "Use server-side identifiers, canonical paths, and a fixed permitted base directory."
79
+ },
80
+ {
81
+ id: "ASA-TLS-001",
82
+ title: "TLS certificate verification disabled",
83
+ severity: "critical",
84
+ category: "transport-security",
85
+ confidence: "high",
86
+ regex: /(?:rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*["']?0)/,
87
+ explanation: "The application disables TLS certificate validation.",
88
+ impact: "An attacker on the network may intercept or modify encrypted traffic.",
89
+ remediation: "Restore certificate validation and configure the correct trusted CA."
90
+ },
91
+ {
92
+ id: "ASA-CORS-001",
93
+ title: "Wildcard CORS origin",
94
+ severity: "medium",
95
+ category: "cors",
96
+ confidence: "low",
97
+ regex: /origin\s*:\s*["']\*["']/,
98
+ explanation: "The CORS configuration appears to allow every web origin.",
99
+ impact: "Untrusted websites may call the application from a victim's browser.",
100
+ remediation: "Use an explicit origin allowlist and reject unknown origins."
101
+ }
102
+ ];
103
+ export function scanDangerousCode(files) {
104
+ const findings = [];
105
+ for (const file of files) {
106
+ const lines = file.content.split(/\r?\n/);
107
+ lines.forEach((line, index) => {
108
+ for (const pattern of CODE_PATTERNS) {
109
+ if (!pattern.regex.test(line)) {
110
+ continue;
111
+ }
112
+ findings.push({
113
+ id: pattern.id,
114
+ title: pattern.title,
115
+ severity: pattern.severity,
116
+ category: pattern.category,
117
+ file: file.relativePath,
118
+ line: index + 1,
119
+ evidence: redactEvidence(line),
120
+ explanation: pattern.explanation,
121
+ impact: pattern.impact,
122
+ remediation: pattern.remediation,
123
+ confidence: pattern.confidence
124
+ });
125
+ }
126
+ });
127
+ }
128
+ return findings;
129
+ }
@@ -0,0 +1,2 @@
1
+ import type { Finding, ProjectFile } from "../types.js";
2
+ export declare function scanSecrets(files: ProjectFile[]): Finding[];
@@ -0,0 +1,125 @@
1
+ import { redactEvidence } from "../utils/evidence.js";
2
+ const SECRET_PATTERNS = [
3
+ {
4
+ id: "ASA-SECRET-001",
5
+ title: "Private key embedded in the project",
6
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
7
+ explanation: "A private cryptographic key appears to be stored directly in a project file.",
8
+ impact: "Anyone who obtains the repository may impersonate the service or decrypt protected data.",
9
+ remediation: "Revoke and rotate the key, remove it from Git history, and use a secret manager."
10
+ },
11
+ {
12
+ id: "ASA-SECRET-002",
13
+ title: "AWS access key detected",
14
+ regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
15
+ explanation: "The file contains a value shaped like an AWS access key.",
16
+ impact: "A leaked cloud credential may permit data theft, infrastructure changes, or unexpected charges.",
17
+ remediation: "Disable and rotate the credential, then use workload identity or a secret manager."
18
+ },
19
+ {
20
+ id: "ASA-SECRET-003",
21
+ title: "GitHub token detected",
22
+ regex: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/,
23
+ explanation: "The file contains a value shaped like a GitHub access token.",
24
+ impact: "The token may grant access to repositories, packages, workflows, or organization data.",
25
+ remediation: "Revoke the token, remove it from Git history, and use short-lived CI credentials."
26
+ },
27
+ {
28
+ id: "ASA-SECRET-004",
29
+ title: "Stripe live secret key detected",
30
+ regex: /\bsk_live_[A-Za-z0-9]{16,}\b/,
31
+ explanation: "The file contains a value shaped like a live Stripe secret key.",
32
+ impact: "An attacker may access payment data or execute privileged API operations.",
33
+ remediation: "Roll the key, remove the old value from Git history, and store the replacement securely."
34
+ }
35
+ ];
36
+ const GENERIC_SECRET_REGEX = /["']?(?:api[_-]?key|client[_-]?secret|jwt[_-]?secret|password|passwd|token)["']?\s*[:=]\s*["'`]([^"'`\r\n]{8,})["'`]/i;
37
+ function looksLikePlaceholder(value) {
38
+ const normalized = value.trim().toLowerCase();
39
+ return [
40
+ "process.env",
41
+ "import.meta.env",
42
+ "${",
43
+ "example",
44
+ "placeholder",
45
+ "changeme",
46
+ "change-me",
47
+ "dummy",
48
+ "your_",
49
+ "your-",
50
+ "<",
51
+ "undefined",
52
+ "null"
53
+ ].some((marker) => normalized.includes(marker));
54
+ }
55
+ function isSensitiveEnvFile(path) {
56
+ const fileName = path.split("/").at(-1) ?? path;
57
+ return (fileName.startsWith(".env") &&
58
+ !fileName.includes("example") &&
59
+ !fileName.includes("sample"));
60
+ }
61
+ export function scanSecrets(files) {
62
+ const findings = [];
63
+ for (const file of files) {
64
+ if (isSensitiveEnvFile(file.relativePath)) {
65
+ findings.push({
66
+ id: "ASA-CONFIG-001",
67
+ title: "Sensitive environment file present",
68
+ severity: "medium",
69
+ category: "secrets",
70
+ file: file.relativePath,
71
+ line: 1,
72
+ evidence: file.relativePath,
73
+ explanation: "A non-example .env file is present in the scan target.",
74
+ impact: "If committed, packaged, or exposed by the web server, application secrets may leak.",
75
+ remediation: "Exclude the file from Git and deployment images. Use a production secret store.",
76
+ confidence: "medium"
77
+ });
78
+ }
79
+ const lines = file.content.split(/\r?\n/);
80
+ lines.forEach((line, index) => {
81
+ let matchedSpecificPattern = false;
82
+ for (const pattern of SECRET_PATTERNS) {
83
+ if (!pattern.regex.test(line)) {
84
+ continue;
85
+ }
86
+ matchedSpecificPattern = true;
87
+ findings.push({
88
+ id: pattern.id,
89
+ title: pattern.title,
90
+ severity: "critical",
91
+ category: "secrets",
92
+ file: file.relativePath,
93
+ line: index + 1,
94
+ evidence: redactEvidence(line),
95
+ explanation: pattern.explanation,
96
+ impact: pattern.impact,
97
+ remediation: pattern.remediation,
98
+ confidence: "high"
99
+ });
100
+ }
101
+ if (matchedSpecificPattern) {
102
+ return;
103
+ }
104
+ const genericMatch = line.match(GENERIC_SECRET_REGEX);
105
+ const candidate = genericMatch?.[1];
106
+ if (!candidate || looksLikePlaceholder(candidate)) {
107
+ return;
108
+ }
109
+ findings.push({
110
+ id: "ASA-SECRET-005",
111
+ title: "Hardcoded credential or secret",
112
+ severity: "high",
113
+ category: "secrets",
114
+ file: file.relativePath,
115
+ line: index + 1,
116
+ evidence: redactEvidence(line),
117
+ explanation: "A password, token, API key, or secret appears to use a literal value.",
118
+ impact: "The credential may leak through source control, backups, logs, build artifacts, or client bundles.",
119
+ remediation: "Move the value to a secret manager or protected environment variable and rotate it.",
120
+ confidence: "medium"
121
+ });
122
+ });
123
+ }
124
+ return findings;
125
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScanOptions, ScanResult } from "./types.js";
2
+ export declare function scanProject(projectRoot: string, options?: ScanOptions): Promise<ScanResult>;
@@ -0,0 +1,91 @@
1
+ import { resolve } from "node:path";
2
+ import { performance } from "node:perf_hooks";
3
+ import { scanDangerousCode } from "./rules/dangerous-code.js";
4
+ import { scanSecrets } from "./rules/secrets.js";
5
+ import { DEFAULT_SCAN_LIMITS, loadProjectFiles } from "./utils/files.js";
6
+ const SEVERITY_ORDER = {
7
+ critical: 4,
8
+ high: 3,
9
+ medium: 2,
10
+ low: 1
11
+ };
12
+ const SCORE_PENALTIES = {
13
+ critical: 30,
14
+ high: 15,
15
+ medium: 7,
16
+ low: 2
17
+ };
18
+ function deduplicateFindings(findings) {
19
+ const unique = new Map();
20
+ for (const finding of findings) {
21
+ const key = `${finding.id}:${finding.file}:${finding.line}`;
22
+ unique.set(key, finding);
23
+ }
24
+ return [...unique.values()];
25
+ }
26
+ function sortFindings(findings) {
27
+ return findings.sort((left, right) => {
28
+ const severityDifference = SEVERITY_ORDER[right.severity] -
29
+ SEVERITY_ORDER[left.severity];
30
+ if (severityDifference !== 0) {
31
+ return severityDifference;
32
+ }
33
+ return (left.file.localeCompare(right.file) ||
34
+ left.line - right.line);
35
+ });
36
+ }
37
+ function buildSummary(findings) {
38
+ const summary = {
39
+ critical: 0,
40
+ high: 0,
41
+ medium: 0,
42
+ low: 0
43
+ };
44
+ for (const finding of findings) {
45
+ summary[finding.severity] += 1;
46
+ }
47
+ return summary;
48
+ }
49
+ function calculateScore(findings) {
50
+ const penalty = findings.reduce((total, finding) => total + SCORE_PENALTIES[finding.severity], 0);
51
+ return Math.max(0, 100 - penalty);
52
+ }
53
+ function calculateVerdict(summary) {
54
+ if (summary.critical > 0 || summary.high > 0) {
55
+ return "BLOCK";
56
+ }
57
+ if (summary.medium > 0 || summary.low > 0) {
58
+ return "REVIEW";
59
+ }
60
+ return "PASS";
61
+ }
62
+ export async function scanProject(projectRoot, options = {}) {
63
+ const startedAt = performance.now();
64
+ const root = resolve(projectRoot);
65
+ const limits = {
66
+ ...DEFAULT_SCAN_LIMITS,
67
+ ...options.limits
68
+ };
69
+ const files = await loadProjectFiles(root, limits);
70
+ const rawFindings = [
71
+ ...scanSecrets(files),
72
+ ...scanDangerousCode(files)
73
+ ];
74
+ if (rawFindings.length > limits.maxFindings) {
75
+ throw new Error(`Scan limit exceeded: findings (${limits.maxFindings})`);
76
+ }
77
+ if (performance.now() - startedAt > limits.timeoutMs) {
78
+ throw new Error(`Scan limit exceeded: timeout (${limits.timeoutMs} ms)`);
79
+ }
80
+ const findings = sortFindings(deduplicateFindings(rawFindings));
81
+ const summary = buildSummary(findings);
82
+ return {
83
+ root,
84
+ scannedFiles: files.length,
85
+ durationMs: Math.round(performance.now() - startedAt),
86
+ score: calculateScore(findings),
87
+ verdict: calculateVerdict(summary),
88
+ summary,
89
+ findings
90
+ };
91
+ }
@@ -0,0 +1,48 @@
1
+ export type Severity = "critical" | "high" | "medium" | "low";
2
+ export type Confidence = "high" | "medium" | "low";
3
+ export type Verdict = "PASS" | "REVIEW" | "BLOCK";
4
+ export interface ProjectFile {
5
+ absolutePath: string;
6
+ relativePath: string;
7
+ content: string;
8
+ }
9
+ export interface ScanLimits {
10
+ maxFiles: number;
11
+ maxTotalBytes: number;
12
+ maxFileBytes: number;
13
+ maxDepth: number;
14
+ maxDirectories: number;
15
+ maxFindings: number;
16
+ timeoutMs: number;
17
+ }
18
+ export interface ScanOptions {
19
+ limits?: Partial<ScanLimits>;
20
+ }
21
+ export interface Finding {
22
+ id: string;
23
+ title: string;
24
+ severity: Severity;
25
+ category: string;
26
+ file: string;
27
+ line: number;
28
+ evidence: string;
29
+ explanation: string;
30
+ impact: string;
31
+ remediation: string;
32
+ confidence: Confidence;
33
+ }
34
+ export interface FindingSummary {
35
+ critical: number;
36
+ high: number;
37
+ medium: number;
38
+ low: number;
39
+ }
40
+ export interface ScanResult {
41
+ root: string;
42
+ scannedFiles: number;
43
+ durationMs: number;
44
+ score: number;
45
+ verdict: Verdict;
46
+ summary: FindingSummary;
47
+ findings: Finding[];
48
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function sanitizeTerminalText(value: string): string;
2
+ export declare function redactEvidence(line: string): string;
@@ -0,0 +1,23 @@
1
+ const MAX_EVIDENCE_LENGTH = 180;
2
+ const ANSI_ESCAPE_SEQUENCE = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|[[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*)?\u0007|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])))/g;
3
+ const CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
4
+ const BIDI_CONTROL_CHARACTERS = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
5
+ export function sanitizeTerminalText(value) {
6
+ return value
7
+ .replace(ANSI_ESCAPE_SEQUENCE, "")
8
+ .replace(/[\r\n\t]+/g, " ")
9
+ .replace(CONTROL_CHARACTERS, "")
10
+ .replace(BIDI_CONTROL_CHARACTERS, "");
11
+ }
12
+ export function redactEvidence(line) {
13
+ const redacted = sanitizeTerminalText(line)
14
+ .trim()
15
+ .replace(/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "AKIA****************")
16
+ .replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "gh*_****************")
17
+ .replace(/\bsk_live_[A-Za-z0-9]{16,}\b/g, "sk_live_****************")
18
+ .replace(/(["']?(?:api[_-]?key|client[_-]?secret|jwt[_-]?secret|password|passwd|token)["']?\s*[:=]\s*["'`])([^"'`]+)(["'`])/gi, "$1[REDACTED]$3");
19
+ if (redacted.length <= MAX_EVIDENCE_LENGTH) {
20
+ return redacted;
21
+ }
22
+ return `${redacted.slice(0, MAX_EVIDENCE_LENGTH)}...`;
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProjectFile, ScanLimits } from "../types.js";
2
+ export declare const DEFAULT_SCAN_LIMITS: ScanLimits;
3
+ export declare function loadProjectFiles(projectRoot: string, limitOverrides?: Partial<ScanLimits>): Promise<ProjectFile[]>;
@@ -0,0 +1,171 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { basename, extname, join, relative, resolve } from "node:path";
3
+ export const DEFAULT_SCAN_LIMITS = {
4
+ maxFiles: 10_000,
5
+ maxTotalBytes: 25_000_000,
6
+ maxFileBytes: 1_000_000,
7
+ maxDepth: 30,
8
+ maxDirectories: 5_000,
9
+ maxFindings: 1_000,
10
+ timeoutMs: 30_000
11
+ };
12
+ const SKIPPED_DIRECTORIES = new Set([
13
+ ".git",
14
+ ".next",
15
+ ".nuxt",
16
+ ".output",
17
+ ".turbo",
18
+ ".vite",
19
+ "build",
20
+ "coverage",
21
+ "dist",
22
+ "fixtures",
23
+ "node_modules",
24
+ "test",
25
+ "tests",
26
+ "vendor"
27
+ ]);
28
+ const ALLOWED_EXTENSIONS = new Set([
29
+ ".cjs",
30
+ ".conf",
31
+ ".env",
32
+ ".html",
33
+ ".ini",
34
+ ".js",
35
+ ".json",
36
+ ".jsx",
37
+ ".mjs",
38
+ ".sh",
39
+ ".toml",
40
+ ".ts",
41
+ ".tsx",
42
+ ".yaml",
43
+ ".yml"
44
+ ]);
45
+ const ALLOWED_FILE_NAMES = new Set([
46
+ "Dockerfile",
47
+ "docker-compose.yaml",
48
+ "docker-compose.yml",
49
+ "package.json"
50
+ ]);
51
+ function normalizePath(path) {
52
+ return path.replaceAll("\\", "/");
53
+ }
54
+ function shouldScanFile(fileName) {
55
+ if (fileName.endsWith(".min.js") || fileName.endsWith(".min.css")) {
56
+ return false;
57
+ }
58
+ if (fileName === "package-lock.json" ||
59
+ fileName === "pnpm-lock.yaml" ||
60
+ fileName === "yarn.lock") {
61
+ return false;
62
+ }
63
+ if (fileName.startsWith(".env")) {
64
+ return true;
65
+ }
66
+ return (ALLOWED_FILE_NAMES.has(fileName) ||
67
+ ALLOWED_EXTENSIONS.has(extname(fileName)));
68
+ }
69
+ function resolveLimits(overrides) {
70
+ const limits = {
71
+ ...DEFAULT_SCAN_LIMITS,
72
+ ...overrides
73
+ };
74
+ for (const [name, value] of Object.entries(limits)) {
75
+ if (!Number.isFinite(value) || value <= 0) {
76
+ throw new Error(`Invalid scan limit ${name}: ${value}`);
77
+ }
78
+ }
79
+ return limits;
80
+ }
81
+ export async function loadProjectFiles(projectRoot, limitOverrides = {}) {
82
+ const limits = resolveLimits(limitOverrides);
83
+ const startedAt = Date.now();
84
+ const root = resolve(projectRoot);
85
+ const rootStats = await stat(root);
86
+ if (!rootStats.isDirectory()) {
87
+ throw new Error(`Scan target is not a directory: ${root}`);
88
+ }
89
+ const files = [];
90
+ let visitedFiles = 0;
91
+ let visitedDirectories = 1;
92
+ let totalBytes = 0;
93
+ function assertWithinTimeLimit() {
94
+ if (Date.now() - startedAt > limits.timeoutMs) {
95
+ throw new Error(`Scan limit exceeded: timeout (${limits.timeoutMs} ms)`);
96
+ }
97
+ }
98
+ async function walk(currentDirectory, depth) {
99
+ assertWithinTimeLimit();
100
+ let entries;
101
+ try {
102
+ entries = await readdir(currentDirectory, {
103
+ withFileTypes: true
104
+ });
105
+ }
106
+ catch {
107
+ return;
108
+ }
109
+ for (const entry of entries) {
110
+ assertWithinTimeLimit();
111
+ if (entry.isSymbolicLink()) {
112
+ continue;
113
+ }
114
+ const absolutePath = join(currentDirectory, entry.name);
115
+ if (entry.isDirectory()) {
116
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
117
+ if (depth + 1 > limits.maxDepth) {
118
+ throw new Error(`Scan limit exceeded: directory depth (${limits.maxDepth})`);
119
+ }
120
+ visitedDirectories += 1;
121
+ if (visitedDirectories > limits.maxDirectories) {
122
+ throw new Error(`Scan limit exceeded: directories (${limits.maxDirectories})`);
123
+ }
124
+ await walk(absolutePath, depth + 1);
125
+ }
126
+ continue;
127
+ }
128
+ if (!entry.isFile()) {
129
+ continue;
130
+ }
131
+ visitedFiles += 1;
132
+ if (visitedFiles > limits.maxFiles) {
133
+ throw new Error(`Scan limit exceeded: files (${limits.maxFiles})`);
134
+ }
135
+ if (!shouldScanFile(entry.name)) {
136
+ continue;
137
+ }
138
+ let fileStats;
139
+ try {
140
+ fileStats = await stat(absolutePath);
141
+ }
142
+ catch {
143
+ continue;
144
+ }
145
+ assertWithinTimeLimit();
146
+ if (fileStats.size > limits.maxFileBytes) {
147
+ continue;
148
+ }
149
+ if (totalBytes + fileStats.size > limits.maxTotalBytes) {
150
+ throw new Error(`Scan limit exceeded: total bytes (${limits.maxTotalBytes})`);
151
+ }
152
+ let content;
153
+ try {
154
+ content = await readFile(absolutePath, "utf8");
155
+ }
156
+ catch {
157
+ // Inaccessible or non-text files are skipped.
158
+ continue;
159
+ }
160
+ totalBytes += fileStats.size;
161
+ assertWithinTimeLimit();
162
+ files.push({
163
+ absolutePath,
164
+ relativePath: normalizePath(relative(root, absolutePath) || basename(absolutePath)),
165
+ content
166
+ });
167
+ }
168
+ }
169
+ await walk(root, 0);
170
+ return files;
171
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@all-safe-projects/allsafe-vibeguard",
3
+ "version": "0.1.0",
4
+ "description": "Local security deployment gate for AI-generated JavaScript and TypeScript applications",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "allsafe-vibe": "dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
24
+ "build": "npm run clean && tsc -p tsconfig.json && node scripts/make-executable.mjs",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "check": "npm run build && npm test",
28
+ "scan": "npm run build && node dist/cli.js scan",
29
+ "security:release": "bash scripts/prepublish-security-audit.sh",
30
+ "prepack": "npm run check",
31
+ "prepublishOnly": "npm run security:release"
32
+ },
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "keywords": [
37
+ "security",
38
+ "vibe-coding",
39
+ "cli",
40
+ "sast",
41
+ "ai-code",
42
+ "vulnerability-scanner",
43
+ "appsec"
44
+ ],
45
+ "author": "All Safe Areas",
46
+ "license": "UNLICENSED",
47
+ "homepage": "https://projects.allsafeareas.org/en-US/sensors-russia/npm-allsafe-vibecode",
48
+ "bugs": {
49
+ "url": "https://allsafeareas.org/security/vulnerability-disclosure"
50
+ },
51
+ "funding": "https://sponsor.allsafeareas.org/",
52
+ "devDependencies": {
53
+ "@types/node": "^26.1.1",
54
+ "typescript": "^7.0.2",
55
+ "vitest": "^4.1.10"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }