@greenarmor/ges-audit-engine 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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +7 -0
  3. package/dist/index.js +87 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/scanners/auth-scanner.d.ts +12 -0
  6. package/dist/scanners/auth-scanner.js +176 -0
  7. package/dist/scanners/auth-scanner.js.map +1 -0
  8. package/dist/scanners/code-security-scanner.d.ts +5 -0
  9. package/dist/scanners/code-security-scanner.js +91 -0
  10. package/dist/scanners/code-security-scanner.js.map +1 -0
  11. package/dist/scanners/config-scanner.d.ts +12 -0
  12. package/dist/scanners/config-scanner.js +210 -0
  13. package/dist/scanners/config-scanner.js.map +1 -0
  14. package/dist/scanners/crypto-scanner.d.ts +5 -0
  15. package/dist/scanners/crypto-scanner.js +92 -0
  16. package/dist/scanners/crypto-scanner.js.map +1 -0
  17. package/dist/scanners/database-scanner.d.ts +7 -0
  18. package/dist/scanners/database-scanner.js +82 -0
  19. package/dist/scanners/database-scanner.js.map +1 -0
  20. package/dist/scanners/secrets-scanner.d.ts +5 -0
  21. package/dist/scanners/secrets-scanner.js +69 -0
  22. package/dist/scanners/secrets-scanner.js.map +1 -0
  23. package/dist/scanners/types.d.ts +22 -0
  24. package/dist/scanners/types.js +2 -0
  25. package/dist/scanners/types.js.map +1 -0
  26. package/package.json +26 -0
  27. package/src/index.ts +97 -0
  28. package/src/scanners/auth-scanner.ts +198 -0
  29. package/src/scanners/code-security-scanner.ts +102 -0
  30. package/src/scanners/config-scanner.ts +224 -0
  31. package/src/scanners/crypto-scanner.ts +103 -0
  32. package/src/scanners/database-scanner.ts +92 -0
  33. package/src/scanners/secrets-scanner.ts +75 -0
  34. package/src/scanners/types.ts +24 -0
  35. package/tsconfig.json +6 -0
  36. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,198 @@
1
+ import type { Scanner, Finding, ScanContext } from "./types.js";
2
+
3
+ const SCAN_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php"]);
4
+
5
+ export class AuthScanner implements Scanner {
6
+ name = "auth";
7
+
8
+ scan(ctx: ScanContext): Finding[] {
9
+ const findings: Finding[] = [];
10
+ const content = ctx.fileContents;
11
+
12
+ const hasAuthMiddleware = this.detectAuthMiddleware(content);
13
+ const routesWithoutAuth = this.detectRoutesWithoutAuth(content, hasAuthMiddleware);
14
+ const hasRateLimiting = this.detectRateLimiting(content);
15
+ const hasSessionConfig = this.detectSessionConfig(content);
16
+ const hasCORSSettings = this.detectCORSSettings(content);
17
+
18
+ if (routesWithoutAuth.length > 0) {
19
+ for (const route of routesWithoutAuth.slice(0, 20)) {
20
+ findings.push({
21
+ ruleId: "AUTH-001",
22
+ severity: "high",
23
+ category: "authentication",
24
+ title: "Route without authentication",
25
+ description: `Endpoint ${route.method} ${route.path} does not require authentication. All endpoints handling personal data must require auth.`,
26
+ file: route.file,
27
+ line: route.line,
28
+ evidence: route.evidence,
29
+ controlIds: ["GDPR-ART32-004", "OWASP-ASVS-003", "OWASP-ASVS-004"],
30
+ fix: "Add authentication middleware to this route or apply globally.",
31
+ });
32
+ }
33
+ }
34
+
35
+ if (!hasRateLimiting) {
36
+ findings.push({
37
+ ruleId: "AUTH-002",
38
+ severity: "high",
39
+ category: "authentication",
40
+ title: "No rate limiting detected",
41
+ description: "No rate limiting library or configuration found. Rate limiting is required on authentication endpoints and API routes.",
42
+ file: "project",
43
+ evidence: "No rate limiter (express-rate-limit, etc.) found in codebase",
44
+ controlIds: ["GDPR-ART32-004", "OWASP-ASVS-003"],
45
+ fix: "Install and configure rate limiting: npm install express-rate-limit",
46
+ });
47
+ }
48
+
49
+ if (!hasSessionConfig) {
50
+ findings.push({
51
+ ruleId: "AUTH-003",
52
+ severity: "medium",
53
+ category: "authentication",
54
+ title: "No session timeout configuration detected",
55
+ description: "No session expiration or timeout configuration found. Sessions must expire after a period of inactivity.",
56
+ file: "project",
57
+ evidence: "No session timeout configuration found",
58
+ controlIds: ["GDPR-ART32-005"],
59
+ fix: "Configure session expiration: maxAge, idle timeout, or JWT expiration.",
60
+ });
61
+ }
62
+
63
+ if (hasCORSSettings === "wildcard") {
64
+ findings.push({
65
+ ruleId: "AUTH-004",
66
+ severity: "high",
67
+ category: "security",
68
+ title: "CORS configured as wildcard (*)",
69
+ description: "CORS is set to allow all origins. This is insecure for production. Restrict to known origins.",
70
+ file: "project",
71
+ evidence: "cors({ origin: '*' }) or Access-Control-Allow-Origin: *",
72
+ controlIds: ["OWASP-ASVS-006"],
73
+ fix: "Restrict CORS to specific origins: cors({ origin: ['https://yourdomain.com'] })",
74
+ });
75
+ }
76
+
77
+ if (!this.detectMFA(content)) {
78
+ findings.push({
79
+ ruleId: "AUTH-005",
80
+ severity: "high",
81
+ category: "authentication",
82
+ title: "No MFA implementation detected",
83
+ description: "No multi-factor authentication implementation found. MFA is mandatory per GDPR Article 32.",
84
+ file: "project",
85
+ evidence: "No MFA/2FA/OTP/TOTP library found in dependencies or code",
86
+ controlIds: ["GDPR-ART32-004"],
87
+ fix: "Implement MFA using TOTP (otpauth, speakeasy) or WebAuthn.",
88
+ });
89
+ }
90
+
91
+ return findings;
92
+ }
93
+
94
+ private detectAuthMiddleware(content: Map<string, string>): boolean {
95
+ const authIndicators = [
96
+ /jwt\.verify|jsonwebtoken|jwtDecode/i,
97
+ /passport\.use|passport\.authenticate/i,
98
+ /authMiddleware|authGuard|requireAuth|isAuthenticated/i,
99
+ /session\s*\(\s*{/i,
100
+ /bearer\s+token/i,
101
+ /firebase.*auth/i,
102
+ /nextAuth|next-auth/i,
103
+ /supabase.*auth/i,
104
+ /clerk/i,
105
+ /auth0/i,
106
+ ];
107
+ return this.searchPatterns(content, authIndicators);
108
+ }
109
+
110
+ private detectRoutesWithoutAuth(
111
+ content: Map<string, string>,
112
+ hasGlobalAuth: boolean,
113
+ ): Array<{ method: string; path: string; file: string; line: number; evidence: string }> {
114
+ const routes: Array<{ method: string; path: string; file: string; line: number; evidence: string }> = [];
115
+
116
+ if (hasGlobalAuth) return routes;
117
+
118
+ const routePattern = /(?:app|router|route)\s*\.\s*(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]*)/gi;
119
+
120
+ for (const [filePath, fileContent] of content) {
121
+ const ext = filePath.substring(filePath.lastIndexOf("."));
122
+ if (!SCAN_EXTENSIONS.has(ext)) continue;
123
+
124
+ const lines = fileContent.split("\n");
125
+ for (let i = 0; i < lines.length; i++) {
126
+ routePattern.lastIndex = 0;
127
+ const match = routePattern.exec(lines[i]);
128
+ if (match) {
129
+ const path = match[2];
130
+ const publicPaths = ["/", "/health", "/healthz", "/status", "/ping", "/ready", "/readiness", "/version", "/public"];
131
+ if (!publicPaths.some(p => path === p)) {
132
+ routes.push({
133
+ method: match[1].toUpperCase(),
134
+ path,
135
+ file: filePath,
136
+ line: i + 1,
137
+ evidence: lines[i].trim(),
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ return routes;
145
+ }
146
+
147
+ private detectRateLimiting(content: Map<string, string>): boolean {
148
+ return this.searchPatterns(content, [
149
+ /rate.?limit/i,
150
+ /rateLimit|rate-limit/i,
151
+ /express-rate-limit/i,
152
+ /throttl/i,
153
+ ]);
154
+ }
155
+
156
+ private detectSessionConfig(content: Map<string, string>): boolean {
157
+ return this.searchPatterns(content, [
158
+ /session\s*\(\s*{[^}]*maxAge/i,
159
+ /maxAge\s*[:=]/i,
160
+ /expiresIn\s*[:=]/i,
161
+ /expires\s*[:=]/i,
162
+ /cookie\s*:\s*{[^}]*maxAge/i,
163
+ /idleTimeout/i,
164
+ ]);
165
+ }
166
+
167
+ private detectCORSSettings(content: Map<string, string>): "wildcard" | "configured" | "none" {
168
+ for (const [, fileContent] of content) {
169
+ if (/cors\s*\(\s*{[^}]*origin\s*:\s*['"]\*['"]/s.test(fileContent) ||
170
+ /Access-Control-Allow-Origin\s*:\s*\*/i.test(fileContent)) {
171
+ return "wildcard";
172
+ }
173
+ if (/cors\s*\(/i.test(fileContent) || /Access-Control-Allow/i.test(fileContent)) {
174
+ return "configured";
175
+ }
176
+ }
177
+ return "none";
178
+ }
179
+
180
+ private detectMFA(content: Map<string, string>): boolean {
181
+ return this.searchPatterns(content, [
182
+ /mfa|multi.?factor|2fa|two.?factor/i,
183
+ /totp|otpauth|speakeasy|otplib/i,
184
+ /webauthn|fido2|passkey/i,
185
+ /authenticator/i,
186
+ ]);
187
+ }
188
+
189
+ private searchPatterns(content: Map<string, string>, patterns: RegExp[]): boolean {
190
+ for (const [, fileContent] of content) {
191
+ for (const pattern of patterns) {
192
+ pattern.lastIndex = 0;
193
+ if (pattern.test(fileContent)) return true;
194
+ }
195
+ }
196
+ return false;
197
+ }
198
+ }
@@ -0,0 +1,102 @@
1
+ import type { Scanner, Finding, ScanContext } from "./types.js";
2
+
3
+ const SQL_INJECTION_PATTERNS = [
4
+ { pattern: /(?:query|execute|raw|sql)\s*\(\s*[`"'].*\+\s*(?:req|params|query|body|input|request)/gi, desc: "SQL query with string concatenation from user input" },
5
+ { pattern: /(?:query|execute|raw|sql)\s*\(\s*[`"'].*\$\{(?:req|params|query|body)/gi, desc: "SQL query with template literal injection" },
6
+ { pattern: /SELECT\s+.*\s+FROM\s+.*\s+WHERE\s+.*\+\s*(?:req|params|query|body)/gi, desc: "SQL SELECT with concatenated user input" },
7
+ { pattern: /INSERT\s+INTO\s+.*VALUES\s*\(.*\+\s*(?:req|params|query|body)/gi, desc: "SQL INSERT with concatenated user input" },
8
+ { pattern: /DELETE\s+FROM\s+.*WHERE\s+.*\+\s*(?:req|params|query|body)/gi, desc: "SQL DELETE with concatenated user input" },
9
+ { pattern: /UPDATE\s+.*SET\s+.*\+\s*(?:req|params|query|body)/gi, desc: "SQL UPDATE with concatenated user input" },
10
+ ];
11
+
12
+ const XSS_PATTERNS = [
13
+ { pattern: /innerHTML\s*=\s*(?:req|params|query|body|input)/gi, desc: "Direct innerHTML assignment from user input" },
14
+ { pattern: /document\.write\s*\(\s*(?:req|params|query|body)/gi, desc: "document.write with user input" },
15
+ { pattern: /v-html\s*=\s*(?:req|params|query|body|input)/gi, desc: "Vue v-html with user input" },
16
+ { pattern: /dangerouslySetInnerHTML\s*=\s*\{.*(?:req|params|query|body)/gi, desc: "React dangerouslySetInnerHTML with user input" },
17
+ { pattern: /\.html\s*\(\s*(?:req|params|query|body)/gi, desc: "jQuery .html() with user input" },
18
+ ];
19
+
20
+ const INPUT_VALIDATION_PATTERNS = [
21
+ { pattern: /(?:parseInt|parseFloat|Number)\s*\(\s*req\.(?:body|params|query)/gi, desc: "Unvalidated number parsing from request" },
22
+ { pattern: /eval\s*\(\s*(?:req|params|query|body|input)/gi, desc: "eval() with user input - critical RCE risk" },
23
+ { pattern: /Function\s*\(\s*(?:req|params|query|body)/gi, desc: "Function constructor with user input" },
24
+ { pattern: /exec\s*\(\s*(?:req|params|query|body)/gi, desc: "Command execution with user input" },
25
+ { pattern: /child_process.*(?:req|params|query|body)/gi, desc: "Child process with user input" },
26
+ ];
27
+
28
+ const SCAN_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php"]);
29
+
30
+ export class CodeSecurityScanner implements Scanner {
31
+ name = "code-security";
32
+
33
+ scan(ctx: ScanContext): Finding[] {
34
+ const findings: Finding[] = [];
35
+
36
+ for (const [filePath, content] of ctx.fileContents) {
37
+ const ext = filePath.substring(filePath.lastIndexOf("."));
38
+ if (!SCAN_EXTENSIONS.has(ext)) continue;
39
+
40
+ const lines = content.split("\n");
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i];
43
+
44
+ for (const { pattern, desc } of SQL_INJECTION_PATTERNS) {
45
+ pattern.lastIndex = 0;
46
+ if (pattern.test(line)) {
47
+ findings.push({
48
+ ruleId: "INJECT-001",
49
+ severity: "critical",
50
+ category: "injection",
51
+ title: "SQL Injection vulnerability",
52
+ description: desc + ". Use parameterized queries or an ORM.",
53
+ file: filePath,
54
+ line: i + 1,
55
+ evidence: line.trim(),
56
+ controlIds: ["OWASP-ASVS-001", "GDPR-ART5-006"],
57
+ fix: "Use parameterized queries: db.query('SELECT * FROM users WHERE id = $1', [req.query.id])",
58
+ });
59
+ }
60
+ }
61
+
62
+ for (const { pattern, desc } of XSS_PATTERNS) {
63
+ pattern.lastIndex = 0;
64
+ if (pattern.test(line)) {
65
+ findings.push({
66
+ ruleId: "INJECT-002",
67
+ severity: "critical",
68
+ category: "xss",
69
+ title: "Cross-Site Scripting (XSS) vulnerability",
70
+ description: desc + ". Sanitize all user input before rendering.",
71
+ file: filePath,
72
+ line: i + 1,
73
+ evidence: line.trim(),
74
+ controlIds: ["OWASP-ASVS-002", "GDPR-ART5-006"],
75
+ fix: "Use textContent instead of innerHTML, or sanitize input with a library like DOMPurify.",
76
+ });
77
+ }
78
+ }
79
+
80
+ for (const { pattern, desc } of INPUT_VALIDATION_PATTERNS) {
81
+ pattern.lastIndex = 0;
82
+ if (pattern.test(line)) {
83
+ findings.push({
84
+ ruleId: "INJECT-003",
85
+ severity: "critical",
86
+ category: "injection",
87
+ title: "Code injection risk",
88
+ description: desc + ". Never pass user input to code execution functions.",
89
+ file: filePath,
90
+ line: i + 1,
91
+ evidence: line.trim(),
92
+ controlIds: ["OWASP-ASVS-001"],
93
+ fix: "Remove eval/exec usage with user input. Use safe alternatives.",
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ return findings;
101
+ }
102
+ }
@@ -0,0 +1,224 @@
1
+ import type { Scanner, Finding, ScanContext } from "./types.js";
2
+
3
+ export class ConfigScanner implements Scanner {
4
+ name = "config";
5
+
6
+ scan(ctx: ScanContext): Finding[] {
7
+ const findings: Finding[] = [];
8
+
9
+ this.checkPackageJson(ctx, findings);
10
+ this.checkEnvFiles(ctx, findings);
11
+ this.checkDockerConfig(ctx, findings);
12
+ this.checkTLSConfig(ctx, findings);
13
+ this.checkGitignore(ctx, findings);
14
+ this.checkLoggingConfig(ctx, findings);
15
+
16
+ return findings;
17
+ }
18
+
19
+ private checkPackageJson(ctx: ScanContext, findings: Finding[]): void {
20
+ const pkgContent = ctx.fileContents.get("package.json");
21
+ if (!pkgContent) return;
22
+
23
+ try {
24
+ const pkg = JSON.parse(pkgContent);
25
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
26
+
27
+ if (deps.helmet === undefined && (deps.express || deps.koa || deps.fastify)) {
28
+ findings.push({
29
+ ruleId: "CONFIG-001",
30
+ severity: "high",
31
+ category: "security",
32
+ title: "Missing security headers (no helmet)",
33
+ description: "No helmet middleware detected for HTTP framework. Security headers protect against XSS, clickjacking, and other attacks.",
34
+ file: "package.json",
35
+ evidence: "helmet not in dependencies",
36
+ controlIds: ["OWASP-ASVS-002", "OWASP-ASVS-006"],
37
+ fix: "npm install helmet && app.use(helmet())",
38
+ });
39
+ }
40
+
41
+ if (deps.cors === undefined && (deps.express || deps.fastify)) {
42
+ findings.push({
43
+ ruleId: "CONFIG-002",
44
+ severity: "medium",
45
+ category: "security",
46
+ title: "No CORS configuration",
47
+ description: "No CORS package found. Unrestricted CORS can expose your API to cross-origin attacks.",
48
+ file: "package.json",
49
+ evidence: "cors not in dependencies",
50
+ controlIds: ["OWASP-ASVS-006"],
51
+ fix: "npm install cors and configure allowed origins explicitly.",
52
+ });
53
+ }
54
+
55
+ const auditDeps = ["express", "lodash", "axios", "underscore"];
56
+ for (const dep of auditDeps) {
57
+ if (deps[dep]) {
58
+ findings.push({
59
+ ruleId: "CONFIG-003",
60
+ severity: "medium",
61
+ category: "dependencies",
62
+ title: `Dependency review needed: ${dep}`,
63
+ description: `${dep} is a commonly exploited dependency. Ensure you are running the latest version with no known vulnerabilities.`,
64
+ file: "package.json",
65
+ evidence: `${dep}: ${deps[dep]}`,
66
+ controlIds: ["CIS-004", "OWASP-ASVS-005"],
67
+ fix: "Run npm audit regularly. Update to latest version. Consider automated dependency scanning.",
68
+ });
69
+ }
70
+ }
71
+ } catch {
72
+ // not valid JSON
73
+ }
74
+ }
75
+
76
+ private checkEnvFiles(ctx: ScanContext, findings: Finding[]): void {
77
+ for (const [filePath, content] of ctx.fileContents) {
78
+ if (filePath !== ".env" && !filePath.endsWith("/.env") && !filePath.startsWith(".env.")) continue;
79
+ if (filePath.includes("example") || filePath.includes("template")) continue;
80
+
81
+ const lines = content.split("\n");
82
+ for (let i = 0; i < lines.length; i++) {
83
+ const line = lines[i].trim();
84
+ if (!line || line.startsWith("#")) continue;
85
+
86
+ if (/\b(PASSWORD|SECRET|KEY|TOKEN|PRIVATE)\b.*=\s*[^\s]/i.test(line) &&
87
+ !line.includes("your_") && !line.includes("changeme") && !line.includes("xxx")) {
88
+ findings.push({
89
+ ruleId: "CONFIG-004",
90
+ severity: "critical",
91
+ category: "secrets",
92
+ title: "Secret with value in .env file",
93
+ description: "A .env file contains actual secret values. Ensure .env files are in .gitignore and never committed.",
94
+ file: filePath,
95
+ line: i + 1,
96
+ evidence: line.split("=")[0] + "=***",
97
+ controlIds: ["OWASP-ASVS-005", "GDPR-ART32-002"],
98
+ fix: "Ensure .env is in .gitignore. Use a secrets management solution for production.",
99
+ });
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ private checkDockerConfig(ctx: ScanContext, findings: Finding[]): void {
106
+ const dockerfile = ctx.fileContents.get("Dockerfile");
107
+ if (dockerfile) {
108
+ if (/USER\s+root/i.test(dockerfile) || (!/USER\s+/i.test(dockerfile))) {
109
+ findings.push({
110
+ ruleId: "CONFIG-005",
111
+ severity: "medium",
112
+ category: "infrastructure",
113
+ title: "Docker running as root",
114
+ description: "Container may be running as root. Use a non-root user for security.",
115
+ file: "Dockerfile",
116
+ evidence: "No non-root USER directive found",
117
+ controlIds: ["CIS-003"],
118
+ fix: "Add: USER node (or other non-root user) to your Dockerfile.",
119
+ });
120
+ }
121
+
122
+ if (/\bENV\b.*(?:PASSWORD|SECRET|KEY|TOKEN)\s*=\s*\S+/i.test(dockerfile)) {
123
+ findings.push({
124
+ ruleId: "CONFIG-006",
125
+ severity: "critical",
126
+ category: "secrets",
127
+ title: "Secret in Dockerfile ENV",
128
+ description: "Secrets must not be baked into Docker images.",
129
+ file: "Dockerfile",
130
+ evidence: "ENV with secret value",
131
+ controlIds: ["OWASP-ASVS-005"],
132
+ fix: "Use Docker secrets or environment variables at runtime instead.",
133
+ });
134
+ }
135
+ }
136
+ }
137
+
138
+ private checkTLSConfig(ctx: ScanContext, findings: Finding[]): void {
139
+ for (const [filePath, content] of ctx.fileContents) {
140
+ if (!filePath.includes(".env") && !filePath.includes("config")) continue;
141
+
142
+ if (/\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0['"]?/i.test(content)) {
143
+ findings.push({
144
+ ruleId: "CONFIG-007",
145
+ severity: "critical",
146
+ category: "encryption",
147
+ title: "TLS verification disabled",
148
+ description: "NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification, enabling MITM attacks.",
149
+ file: filePath,
150
+ evidence: "NODE_TLS_REJECT_UNAUTHORIZED=0",
151
+ controlIds: ["GDPR-ART32-003", "OWASP-ASVS-006"],
152
+ fix: "Remove NODE_TLS_REJECT_UNAUTHORIZED=0. Fix the certificate issue instead.",
153
+ });
154
+ }
155
+ }
156
+ }
157
+
158
+ private checkGitignore(ctx: ScanContext, findings: Finding[]): void {
159
+ const gitignore = ctx.fileContents.get(".gitignore");
160
+ if (!gitignore) {
161
+ findings.push({
162
+ ruleId: "CONFIG-008",
163
+ severity: "high",
164
+ category: "security",
165
+ title: "No .gitignore file",
166
+ description: "No .gitignore found. Secrets and build artifacts may be committed accidentally.",
167
+ file: ".gitignore",
168
+ evidence: "File not found",
169
+ controlIds: ["OWASP-ASVS-005"],
170
+ fix: "Create .gitignore with node_modules/, .env, dist/, *.key, etc.",
171
+ });
172
+ return;
173
+ }
174
+
175
+ const required = [".env", "node_modules"];
176
+ for (const pattern of required) {
177
+ if (!gitignore.includes(pattern)) {
178
+ findings.push({
179
+ ruleId: "CONFIG-009",
180
+ severity: "high",
181
+ category: "security",
182
+ title: `.gitignore missing ${pattern}`,
183
+ description: `${pattern} should be in .gitignore to prevent accidental commits.`,
184
+ file: ".gitignore",
185
+ evidence: `${pattern} not found in .gitignore`,
186
+ controlIds: ["OWASP-ASVS-005"],
187
+ fix: `Add ${pattern} to .gitignore.`,
188
+ });
189
+ }
190
+ }
191
+ }
192
+
193
+ private checkLoggingConfig(ctx: ScanContext, findings: Finding[]): void {
194
+ const hasLogging = this.searchContent(ctx, [
195
+ /winston|pino|bunyan|morgan|helmet/i,
196
+ /logging|logger/i,
197
+ /auditLog|audit_log/i,
198
+ ]);
199
+
200
+ if (!hasLogging) {
201
+ findings.push({
202
+ ruleId: "CONFIG-010",
203
+ severity: "high",
204
+ category: "audit",
205
+ title: "No logging framework detected",
206
+ description: "No logging library or audit logging found. Audit logging is mandatory for GDPR compliance.",
207
+ file: "project",
208
+ evidence: "No logging library (winston, pino, etc.) found",
209
+ controlIds: ["GDPR-ART32-006", "OWASP-ASVS-004"],
210
+ fix: "Install a logging library (winston or pino) and implement structured audit logging.",
211
+ });
212
+ }
213
+ }
214
+
215
+ private searchContent(ctx: ScanContext, patterns: RegExp[]): boolean {
216
+ for (const [, content] of ctx.fileContents) {
217
+ for (const pattern of patterns) {
218
+ pattern.lastIndex = 0;
219
+ if (pattern.test(content)) return true;
220
+ }
221
+ }
222
+ return false;
223
+ }
224
+ }
@@ -0,0 +1,103 @@
1
+ import type { Scanner, Finding, ScanContext } from "./types.js";
2
+
3
+ const WEAK_HASH_PATTERNS = [
4
+ { pattern: /\bmd5\s*\(/gi, algo: "MD5" },
5
+ { pattern: /\bsha1\s*\(/gi, algo: "SHA1" },
6
+ { pattern: /\bcreateHash\s*\(\s*['"]md5['"]\s*\)/gi, algo: "MD5 (Node.js crypto)" },
7
+ { pattern: /\bcreateHash\s*\(\s*['"]sha1['"]\s*\)/gi, algo: "SHA1 (Node.js crypto)" },
8
+ { pattern: /\.digest\s*\(\s*['"]md5['"]\s*\)/gi, algo: "MD5 digest" },
9
+ { pattern: /hashlib\.md5\(/gi, algo: "MD5 (Python)" },
10
+ { pattern: /hashlib\.sha1\(/gi, algo: "SHA1 (Python)" },
11
+ ];
12
+
13
+ const WEAK_CRYPTO_PATTERNS = [
14
+ { pattern: /\bDES\b|\b3DES\b|\bBlowfish\b/g, algo: "Weak encryption algorithm" },
15
+ { pattern: /\bcreateCipheriv\s*\(\s*['"]aes-128/gi, algo: "AES-128 (use AES-256)" },
16
+ { pattern: /\bcreateCipher\b\s*\(/g, algo: "Deprecated createCipher (use createCipheriv)" },
17
+ { pattern: /\btc_aes_encrypt\b/gi, algo: "AES-128 (use AES-256)" },
18
+ { pattern: /\bAES.*ECB\b/gi, algo: "AES ECB mode (use GCM or CBC)" },
19
+ { pattern: /Cipher\s*\(\s*['"]des/gi, algo: "DES cipher (deprecated)" },
20
+ { pattern: /\btls\.connect\s*\([^)]*rejectUnauthorized\s*:\s*false/gi, algo: "TLS with certificate verification disabled" },
21
+ { pattern: /process\.env\.NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]0['"]/gi, algo: "TLS verification globally disabled" },
22
+ ];
23
+
24
+ const INSECURE_PASSWORD_PATTERNS = [
25
+ { pattern: /\.compare\s*\(.*,\s*.*\)|bcrypt\.compare|argon2\.verify/gi, check: false, desc: "Secure password comparison" },
26
+ { pattern: /password\s*===?\s*|password\s*!==?\s*|\.equals\s*\(\s*password/gi, check: true, desc: "Plaintext password comparison (use Argon2id/bcrypt)" },
27
+ ];
28
+
29
+ const SCAN_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php", ".cs"]);
30
+
31
+ export class CryptoScanner implements Scanner {
32
+ name = "crypto";
33
+
34
+ scan(ctx: ScanContext): Finding[] {
35
+ const findings: Finding[] = [];
36
+
37
+ for (const [filePath, content] of ctx.fileContents) {
38
+ const ext = filePath.substring(filePath.lastIndexOf("."));
39
+ if (!SCAN_EXTENSIONS.has(ext)) continue;
40
+
41
+ const lines = content.split("\n");
42
+ for (let i = 0; i < lines.length; i++) {
43
+ const line = lines[i];
44
+
45
+ for (const { pattern, algo } of WEAK_HASH_PATTERNS) {
46
+ pattern.lastIndex = 0;
47
+ if (pattern.test(line)) {
48
+ findings.push({
49
+ ruleId: "CRYPTO-001",
50
+ severity: "critical",
51
+ category: "authentication",
52
+ title: `Weak hashing algorithm: ${algo}`,
53
+ description: `${algo} is cryptographically broken and must not be used for passwords or security-sensitive operations. Use Argon2id for passwords, SHA-256+ for general hashing.`,
54
+ file: filePath,
55
+ line: i + 1,
56
+ evidence: line.trim(),
57
+ controlIds: ["GDPR-ART32-004", "OWASP-ASVS-003"],
58
+ fix: `Replace ${algo} with Argon2id (passwords) or SHA-256+ (general hashing).`,
59
+ });
60
+ }
61
+ }
62
+
63
+ for (const { pattern, algo } of WEAK_CRYPTO_PATTERNS) {
64
+ pattern.lastIndex = 0;
65
+ if (pattern.test(line)) {
66
+ findings.push({
67
+ ruleId: "CRYPTO-002",
68
+ severity: "high",
69
+ category: "encryption",
70
+ title: `Insecure encryption: ${algo}`,
71
+ description: `${algo} is not approved for use. Use AES-256-GCM or ChaCha20-Poly1305.`,
72
+ file: filePath,
73
+ line: i + 1,
74
+ evidence: line.trim(),
75
+ controlIds: ["GDPR-ART32-002", "GDPR-ART32-003"],
76
+ fix: "Replace with AES-256-GCM or ChaCha20-Poly1305 for data at rest, TLS 1.3 for data in transit.",
77
+ });
78
+ }
79
+ }
80
+
81
+ for (const { pattern, check, desc } of INSECURE_PASSWORD_PATTERNS) {
82
+ pattern.lastIndex = 0;
83
+ if (check && pattern.test(line)) {
84
+ findings.push({
85
+ ruleId: "CRYPTO-003",
86
+ severity: "critical",
87
+ category: "authentication",
88
+ title: desc,
89
+ description: "Passwords must be hashed using Argon2id before comparison. Never compare plaintext passwords.",
90
+ file: filePath,
91
+ line: i + 1,
92
+ evidence: line.trim(),
93
+ controlIds: ["GDPR-ART32-004", "OWASP-ASVS-003"],
94
+ fix: "Use argon2.verify(hashedPassword, inputPassword) for password comparison.",
95
+ });
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ return findings;
102
+ }
103
+ }