@greenarmor/ges-audit-engine 1.0.1 → 1.1.1

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/dist/index.d.ts CHANGED
@@ -4,4 +4,16 @@ export declare function runAudit(root: string): {
4
4
  findings: Finding[];
5
5
  scannedFiles: number;
6
6
  };
7
+ export interface AuditCache {
8
+ [filePath: string]: {
9
+ hash: string;
10
+ findings: Finding[];
11
+ };
12
+ }
13
+ export declare function runAuditIncremental(root: string, cache?: AuditCache): {
14
+ findings: Finding[];
15
+ scannedFiles: number;
16
+ newCache: AuditCache;
17
+ changedFiles: number;
18
+ };
7
19
  export declare function deduplicateFindings(findings: Finding[]): Finding[];
package/dist/index.js CHANGED
@@ -6,10 +6,11 @@ import { CodeSecurityScanner } from "./scanners/code-security-scanner.js";
6
6
  import { AuthScanner } from "./scanners/auth-scanner.js";
7
7
  import { ConfigScanner } from "./scanners/config-scanner.js";
8
8
  import { DatabaseScanner } from "./scanners/database-scanner.js";
9
+ import { IaCScanner } from "./scanners/iac-scanner.js";
9
10
  const IGNORE_DIRS = new Set([
10
11
  "node_modules", ".git", "dist", "build", ".next", ".nuxt", "coverage",
11
12
  ".ges", "vendor", "__pycache__", ".venv", "venv", ".turbo", ".cache",
12
- "reports", "compliance", "security", "controls", "policies", "checklists", "docs",
13
+ "reports",
13
14
  "bundle", ".crush", ".vscode", ".idea",
14
15
  ]);
15
16
  const SKIP_PATHS = [
@@ -20,8 +21,48 @@ const IGNORE_EXTENSIONS = new Set([
20
21
  ".woff2", ".ttf", ".eot", ".mp4", ".mp3", ".zip", ".gz", ".tar",
21
22
  ".lock", ".map", ".wasm",
22
23
  ]);
24
+ function loadGesIgnore(root) {
25
+ const ignorePath = path.join(root, ".gesignore");
26
+ if (!fs.existsSync(ignorePath))
27
+ return [];
28
+ try {
29
+ const content = fs.readFileSync(ignorePath, "utf-8");
30
+ return content
31
+ .split("\n")
32
+ .map(line => line.trim())
33
+ .filter(line => line.length > 0 && !line.startsWith("#"));
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ }
39
+ function isIgnored(filePath, patterns) {
40
+ for (const pattern of patterns) {
41
+ if (pattern.endsWith("/")) {
42
+ const dir = pattern.slice(0, -1);
43
+ if (filePath === dir || filePath.startsWith(dir + "/"))
44
+ return true;
45
+ }
46
+ else if (pattern.startsWith("*.")) {
47
+ const ext = pattern.slice(1);
48
+ if (filePath.endsWith(ext))
49
+ return true;
50
+ }
51
+ else if (pattern.includes("*")) {
52
+ const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
53
+ if (regex.test(filePath))
54
+ return true;
55
+ }
56
+ else {
57
+ if (filePath === pattern || filePath.startsWith(pattern + "/"))
58
+ return true;
59
+ }
60
+ }
61
+ return false;
62
+ }
23
63
  function collectFiles(root) {
24
64
  const files = [];
65
+ const ignorePatterns = loadGesIgnore(root);
25
66
  function walk(dir) {
26
67
  const entries = fs.readdirSync(dir, { withFileTypes: true });
27
68
  for (const entry of entries) {
@@ -36,7 +77,9 @@ function collectFiles(root) {
36
77
  if (!IGNORE_EXTENSIONS.has(ext)) {
37
78
  const rel = path.relative(root, fullPath).replace(/\\/g, "/");
38
79
  if (!SKIP_PATHS.some(skip => rel.includes(skip))) {
39
- files.push(rel);
80
+ if (!isIgnored(rel, ignorePatterns)) {
81
+ files.push(rel);
82
+ }
40
83
  }
41
84
  }
42
85
  }
@@ -149,6 +192,7 @@ export function runAudit(root) {
149
192
  new AuthScanner(),
150
193
  new ConfigScanner(),
151
194
  new DatabaseScanner(),
195
+ new IaCScanner(),
152
196
  ];
153
197
  const allFindings = [];
154
198
  for (const scanner of scanners) {
@@ -156,6 +200,87 @@ export function runAudit(root) {
156
200
  }
157
201
  return { findings: allFindings, scannedFiles: files.length };
158
202
  }
203
+ export function runAuditIncremental(root, cache) {
204
+ const files = collectFiles(root);
205
+ const fileContents = readFiles(root, files);
206
+ const oldCache = cache || {};
207
+ const newCache = {};
208
+ const changedFiles = [];
209
+ for (const file of files) {
210
+ const content = fileContents.get(file) || "";
211
+ const hash = simpleHash(content);
212
+ if (oldCache[file] && oldCache[file].hash === hash) {
213
+ newCache[file] = oldCache[file];
214
+ }
215
+ else {
216
+ changedFiles.push(file);
217
+ newCache[file] = { hash, findings: [] };
218
+ }
219
+ }
220
+ const isWebProject = detectWebProject(fileContents);
221
+ const changedContents = new Map();
222
+ for (const file of changedFiles) {
223
+ changedContents.set(file, fileContents.get(file) || "");
224
+ }
225
+ const ctx = {
226
+ root,
227
+ files: changedFiles,
228
+ fileContents: changedContents,
229
+ isWebProject,
230
+ };
231
+ const fullCtx = {
232
+ root,
233
+ files,
234
+ fileContents,
235
+ isWebProject,
236
+ };
237
+ const perFileScanners = [
238
+ new SecretsScanner(),
239
+ new CryptoScanner(),
240
+ new CodeSecurityScanner(),
241
+ new DatabaseScanner(),
242
+ new IaCScanner(),
243
+ ];
244
+ const projectScanners = [
245
+ new AuthScanner(),
246
+ new ConfigScanner(),
247
+ ];
248
+ const changedFindings = [];
249
+ for (const scanner of perFileScanners) {
250
+ changedFindings.push(...scanner.scan(ctx));
251
+ }
252
+ for (const finding of changedFindings) {
253
+ if (newCache[finding.file]) {
254
+ newCache[finding.file].findings.push(finding);
255
+ }
256
+ }
257
+ const projectFindings = [];
258
+ for (const scanner of projectScanners) {
259
+ projectFindings.push(...scanner.scan(fullCtx));
260
+ }
261
+ const allFindings = [];
262
+ for (const file of files) {
263
+ if (newCache[file]) {
264
+ allFindings.push(...newCache[file].findings);
265
+ }
266
+ }
267
+ allFindings.push(...projectFindings);
268
+ return {
269
+ findings: allFindings,
270
+ scannedFiles: files.length,
271
+ newCache,
272
+ changedFiles: changedFiles.length,
273
+ };
274
+ }
275
+ function simpleHash(content) {
276
+ let hash = 0;
277
+ for (let i = 0; i < content.length; i++) {
278
+ const char = content.charCodeAt(i);
279
+ hash = (hash << 5) - hash + char;
280
+ hash |= 0;
281
+ }
282
+ return hash.toString(36);
283
+ }
159
284
  export function deduplicateFindings(findings) {
160
285
  const seen = new Set();
161
286
  return findings.filter(f => {
@@ -5,6 +5,12 @@ const SQL_INJECTION_PATTERNS = [
5
5
  { pattern: /INSERT\s+INTO\s+.*VALUES\s*\(.*\+\s*(?:req|params|query|body)/gi, desc: "SQL INSERT with concatenated user input" },
6
6
  { pattern: /DELETE\s+FROM\s+.*WHERE\s+.*\+\s*(?:req|params|query|body)/gi, desc: "SQL DELETE with concatenated user input" },
7
7
  { pattern: /UPDATE\s+.*SET\s+.*\+\s*(?:req|params|query|body)/gi, desc: "SQL UPDATE with concatenated user input" },
8
+ { pattern: /(?:execute|cursor\.execute)\s*\(\s*f['"].*\{(?:request|form|args|GET|POST)/gi, desc: "Python f-string SQL injection from request input" },
9
+ { pattern: /(?:execute|cursor\.execute)\s*\(\s*['"].*%s.*['"].*\+\s*(?:request|form|args)/gi, desc: "Python SQL with string concatenation" },
10
+ { pattern: /fmt\.Sprintf\s*\(\s*['"].*SELECT.*['"].*,\s*(?:r\.URL|r\.Form|req\.)/gi, desc: "Go SQL injection via fmt.Sprintf with request input" },
11
+ { pattern: /db\.(Query|Exec|MustExec)\s*\(\s*fmt\.Sprintf/gi, desc: "Go SQL query with fmt.Sprintf (potential injection)" },
12
+ { pattern: /createQuery\s*\(\s*['"].*\+\s*(?:request|getParameter|req\.)/gi, desc: "Java SQL with string concatenation" },
13
+ { pattern: /jdbcTemplate.*\+\s*(?:request|getParameter)/gi, desc: "Java Spring SQL injection" },
8
14
  ];
9
15
  const XSS_PATTERNS = [
10
16
  { pattern: /innerHTML\s*=\s*(?:req|params|query|body|input)/gi, desc: "Direct innerHTML assignment from user input" },
@@ -19,8 +25,15 @@ const INPUT_VALIDATION_PATTERNS = [
19
25
  { pattern: new RegExp(["F", "u", "n", "c", "t", "i", "o", "n"].join("") + "\\s*\\(\\s*(?:req|params|query|body)", "gi"), desc: ["F", "u", "n", "c", "t", "i", "o", "n"].join("") + " constructor with user input" },
20
26
  { pattern: /exec\s*\(\s*(?:req|params|query|body)/gi, desc: "Command execution with user input" },
21
27
  { pattern: /child_process.*(?:req|params|query|body)/gi, desc: "Child process with user input" },
28
+ { pattern: /os\.system\s*\(\s*(?:request|form|args|GET|POST|input)/gi, desc: "Python os.system with user input" },
29
+ { pattern: /subprocess\.(?:call|run|Popen)\s*\(\s*['"].*\+\s*(?:request|form|args)/gi, desc: "Python subprocess with string concatenation" },
30
+ { pattern: /eval\s*\(\s*(?:request|form|args|GET|POST|input)/gi, desc: "Python eval with user input" },
31
+ { pattern: /exec\.Command\s*\(\s*['"].*['"].*,\s*(?:r\.URL|r\.Form|req\.)/gi, desc: "Go exec.Command with request input" },
32
+ { pattern: /Runtime\.getRuntime\s*\(\s*\)\.exec\s*\(\s*(?:request|getParameter|req\.)/gi, desc: "Java Runtime.exec with request input" },
33
+ { pattern: /ProcessBuilder.*(?:request|getParameter|args)/gi, desc: "Java ProcessBuilder with request input" },
34
+ { pattern: /Command::new\s*\(\s*['"].*['"].*\.(?:arg|args)\s*\(\s*(?:req|request|input)/gi, desc: "Rust Command with user input" },
22
35
  ];
23
- const SCAN_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php"]);
36
+ const SCAN_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php", ".rs", ".cs"]);
24
37
  export class CodeSecurityScanner {
25
38
  name = "code-security";
26
39
  scan(ctx) {
@@ -6,6 +6,13 @@ const WEAK_HASH_PATTERNS = [
6
6
  { pattern: /\.digest\s*\(\s*['"]md5['"]\s*\)/gi, algo: "MD5 digest" },
7
7
  { pattern: /hashlib\.md5\(/gi, algo: "MD5 (Python)" },
8
8
  { pattern: /hashlib\.sha1\(/gi, algo: "SHA1 (Python)" },
9
+ { pattern: /crypto\/md5\.New\(|md5\.New\(/gi, algo: "MD5 (Go)" },
10
+ { pattern: /crypto\/sha1\.New\(|sha1\.New\(/gi, algo: "SHA1 (Go)" },
11
+ { pattern: /MessageDigest\.getInstance\s*\(\s*['"]MD5['"]\s*\)/gi, algo: "MD5 (Java)" },
12
+ { pattern: /MessageDigest\.getInstance\s*\(\s*['"]SHA-?1['"]\s*\)/gi, algo: "SHA1 (Java)" },
13
+ { pattern: /md5::compute\(/gi, algo: "MD5 (Rust)" },
14
+ { pattern: /sha1::Sha1/gi, algo: "SHA1 (Rust)" },
15
+ { pattern: /Digest::new\s*\(\s*\)/gi, algo: "Potential weak digest (Rust)" },
9
16
  ];
10
17
  const WEAK_CRYPTO_PATTERNS = [
11
18
  { pattern: /\bDES\b|\b3DES\b|\bBlowfish\b/g, algo: "Weak encryption algorithm" },
@@ -13,16 +20,23 @@ const WEAK_CRYPTO_PATTERNS = [
13
20
  { pattern: /\bcreateCipher\b\s*\(/g, algo: "Deprecated createCipher (use createCipheriv)" },
14
21
  { pattern: /\btc_aes_encrypt\b/gi, algo: "AES-128 (use AES-256)" },
15
22
  { pattern: /\bAES.*ECB\b/gi, algo: "AES ECB mode (use GCM or CBC)" },
16
- { pattern: /Cipher\s*\(\s*['"]des/gi, algo: "DES cipher (deprecated)" },
23
+ { pattern: /Cipher\.getInstance\s*\(\s*['"]DES/gi, algo: "DES cipher (Java, deprecated)" },
24
+ { pattern: /des\.new\s*\(/gi, algo: "DES cipher (Rust, deprecated)" },
25
+ { pattern: /crypto\/des\.NewCipher\s*\(/gi, algo: "DES cipher (Go, deprecated)" },
26
+ { pattern: /Crypto\.Cipher\.DES/gi, algo: "DES cipher (Python, deprecated)" },
17
27
  { pattern: /\btls\.connect\s*\([^)]*rejectUnauthorized\s*:\s*false/gi, algo: "TLS with certificate verification disabled" },
18
28
  { pattern: /process\.env\.NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]0['"]/gi, algo: "TLS verification globally disabled" },
29
+ { pattern: /InsecureSkipVerify\s*:\s*true/gi, algo: "TLS verification disabled (Go)" },
30
+ { pattern: /verify_mode\s*=\s*ssl\.CERT_NONE/gi, algo: "TLS verification disabled (Python)" },
31
+ { pattern: /TrustAllCerts|TrustManager.*X509TrustManager/gi, algo: "TLS verification disabled (Java)" },
32
+ { pattern: /danger_accept_invalid_certs\s*\(\s*true/gi, algo: "TLS verification disabled (Rust)" },
19
33
  ];
20
34
  const INSECURE_PASSWORD_PATTERNS = [
21
35
  { pattern: /\.compare\s*\(.*,\s*.*\)|bcrypt\.compare|argon2\.verify/gi, check: false, desc: "Secure password comparison" },
22
36
  { pattern: /(?:stored|saved|hashed|db|database)\s*\.?\s*(?:password|pw)\s*===?\s*(?:req|input|user|plain|raw)/gi, check: true, desc: "Plaintext password comparison (use Argon2id/bcrypt)" },
23
37
  { pattern: /(?:password|pw)\s*===?\s*['"][^'"]{2,}['"]/gi, check: true, desc: "Hardcoded password comparison (use Argon2id/bcrypt)" },
24
38
  ];
25
- const SCAN_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php", ".cs"]);
39
+ const SCAN_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php", ".cs", ".rs"]);
26
40
  export class CryptoScanner {
27
41
  name = "crypto";
28
42
  scan(ctx) {
@@ -0,0 +1,11 @@
1
+ import type { Scanner, Finding, ScanContext } from "./types.js";
2
+ export declare class IaCScanner implements Scanner {
3
+ name: string;
4
+ scan(ctx: ScanContext): Finding[];
5
+ private checkTerraform;
6
+ private checkS3Buckets;
7
+ private checkSecurityGroups;
8
+ private checkDatabases;
9
+ private checkIAMPolicies;
10
+ private checkKMSKeys;
11
+ }
@@ -0,0 +1,278 @@
1
+ const IAC_EXTENSIONS = new Set([".tf", ".tfvars", ".cfn", ".yaml", ".yml", ".json", ".dockerfile"]);
2
+ export class IaCScanner {
3
+ name = "iac";
4
+ scan(ctx) {
5
+ const findings = [];
6
+ this.checkTerraform(ctx, findings);
7
+ this.checkS3Buckets(ctx, findings);
8
+ this.checkSecurityGroups(ctx, findings);
9
+ this.checkDatabases(ctx, findings);
10
+ this.checkIAMPolicies(ctx, findings);
11
+ this.checkKMSKeys(ctx, findings);
12
+ return findings;
13
+ }
14
+ checkTerraform(ctx, findings) {
15
+ for (const [filePath, content] of ctx.fileContents) {
16
+ if (!filePath.endsWith(".tf"))
17
+ continue;
18
+ const lines = content.split("\n");
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i].toLowerCase();
21
+ if (line.includes("force_destroy") && line.includes("true")) {
22
+ findings.push({
23
+ ruleId: "IAC-001",
24
+ severity: "medium",
25
+ category: "infrastructure",
26
+ title: "S3 bucket force_destroy enabled",
27
+ description: "force_destroy will permanently delete all objects in the bucket when destroyed. This can cause unintended data loss.",
28
+ file: filePath,
29
+ line: i + 1,
30
+ evidence: lines[i].trim(),
31
+ controlIds: ["GDPR-ART32-008", "ISO27001-A17"],
32
+ fix: "Set force_destroy to false unless this is a temporary bucket.",
33
+ });
34
+ }
35
+ if (line.includes("0.0.0.0/0") && (line.includes("ingress") || line.includes("cidr_blocks"))) {
36
+ findings.push({
37
+ ruleId: "IAC-002",
38
+ severity: "critical",
39
+ category: "infrastructure",
40
+ title: "Security group open to the entire internet (0.0.0.0/0)",
41
+ description: "Security group rule allows traffic from any IP address. This exposes the resource to the entire internet.",
42
+ file: filePath,
43
+ line: i + 1,
44
+ evidence: lines[i].trim(),
45
+ controlIds: ["OWASP-ASVS-006", "ISO27001-A9"],
46
+ fix: "Restrict cidr_blocks to specific IP ranges instead of 0.0.0.0/0.",
47
+ });
48
+ }
49
+ if (line.includes("ssl") && (line.includes("false") || line.includes("disabled"))) {
50
+ findings.push({
51
+ ruleId: "IAC-003",
52
+ severity: "high",
53
+ category: "encryption",
54
+ title: "SSL/TLS disabled on resource",
55
+ description: "SSL/TLS is explicitly disabled. Data in transit will be unencrypted.",
56
+ file: filePath,
57
+ line: i + 1,
58
+ evidence: lines[i].trim(),
59
+ controlIds: ["GDPR-ART32-002", "HIPAA-164.312-e"],
60
+ fix: "Enable SSL/TLS encryption for all data in transit.",
61
+ });
62
+ }
63
+ }
64
+ }
65
+ }
66
+ checkS3Buckets(ctx, findings) {
67
+ for (const [filePath, content] of ctx.fileContents) {
68
+ if (!filePath.endsWith(".tf") && !filePath.endsWith(".yaml") && !filePath.endsWith(".yml"))
69
+ continue;
70
+ if (!content.includes("aws_s3_bucket") && !content.includes("AWS::S3::Bucket"))
71
+ continue;
72
+ if (content.includes("acl") && (content.match(/acl\s*=\s*["']public-read["']/) || content.match(/AccessControl.*PublicRead/))) {
73
+ findings.push({
74
+ ruleId: "IAC-004",
75
+ severity: "critical",
76
+ category: "infrastructure",
77
+ title: "S3 bucket set to public-read",
78
+ description: "S3 bucket ACL is set to public-read. Anyone on the internet can read the contents.",
79
+ file: filePath,
80
+ evidence: "acl = public-read or AccessControl: PublicRead",
81
+ controlIds: ["GDPR-ART32-002", "OWASP-ASVS-006"],
82
+ fix: "Set bucket ACL to private. Use presigned URLs for temporary access.",
83
+ });
84
+ }
85
+ if (!content.includes("server_side_encryption") && !content.includes("BucketEncryption")) {
86
+ findings.push({
87
+ ruleId: "IAC-005",
88
+ severity: "high",
89
+ category: "encryption",
90
+ title: "S3 bucket without server-side encryption",
91
+ description: "S3 bucket does not have server-side encryption configured. Data at rest is unencrypted.",
92
+ file: filePath,
93
+ evidence: "No server_side_encryption or BucketEncryption block found",
94
+ controlIds: ["GDPR-ART32-001", "HIPAA-164.312-a"],
95
+ fix: "Add server_side_encryption_configuration with AES-256 or AWS KMS.",
96
+ });
97
+ }
98
+ if (!content.includes("versioning") && !content.includes("VersioningConfiguration")) {
99
+ findings.push({
100
+ ruleId: "IAC-006",
101
+ severity: "medium",
102
+ category: "infrastructure",
103
+ title: "S3 bucket without versioning",
104
+ description: "S3 bucket does not have versioning enabled. Accidental deletions cannot be recovered.",
105
+ file: filePath,
106
+ evidence: "No versioning or VersioningConfiguration block found",
107
+ controlIds: ["GDPR-ART32-008", "ISO27001-A8"],
108
+ fix: "Enable versioning on the bucket to protect against accidental data loss.",
109
+ });
110
+ }
111
+ }
112
+ }
113
+ checkSecurityGroups(ctx, findings) {
114
+ for (const [filePath, content] of ctx.fileContents) {
115
+ if (!filePath.endsWith(".tf") && !filePath.endsWith(".yaml") && !filePath.endsWith(".yml"))
116
+ continue;
117
+ if (!content.includes("security") && !content.includes("SecurityGroup"))
118
+ continue;
119
+ const lines = content.split("\n");
120
+ for (let i = 0; i < lines.length; i++) {
121
+ const line = lines[i].toLowerCase();
122
+ if ((line.includes("from_port") && line.match(/\b22\b/)) || (line.includes("port") && line.match(/\b22\b/))) {
123
+ const fullBlock = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 10)).join(" ").toLowerCase();
124
+ if (fullBlock.includes("0.0.0.0/0")) {
125
+ findings.push({
126
+ ruleId: "IAC-007",
127
+ severity: "critical",
128
+ category: "infrastructure",
129
+ title: "SSH (port 22) open to the internet",
130
+ description: "Security group allows SSH access from 0.0.0.0/0. This is a common attack vector.",
131
+ file: filePath,
132
+ line: i + 1,
133
+ evidence: lines[i].trim(),
134
+ controlIds: ["OWASP-ASVS-006", "ISO27001-A9", "CIS-005"],
135
+ fix: "Restrict SSH access to specific IP ranges or use a bastion host.",
136
+ });
137
+ }
138
+ }
139
+ if ((line.includes("from_port") && line.match(/\b3306\b/)) || (line.includes("port") && line.match(/\b3306\b/))) {
140
+ const fullBlock = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 10)).join(" ").toLowerCase();
141
+ if (fullBlock.includes("0.0.0.0/0")) {
142
+ findings.push({
143
+ ruleId: "IAC-008",
144
+ severity: "critical",
145
+ category: "infrastructure",
146
+ title: "Database (port 3306) open to the internet",
147
+ description: "Security group allows MySQL access from 0.0.0.0/0. Databases should never be publicly accessible.",
148
+ file: filePath,
149
+ line: i + 1,
150
+ evidence: lines[i].trim(),
151
+ controlIds: ["GDPR-ART32-002", "OWASP-ASVS-006"],
152
+ fix: "Restrict database access to application servers only.",
153
+ });
154
+ }
155
+ }
156
+ if ((line.includes("from_port") && line.match(/\b5432\b/)) || (line.includes("port") && line.match(/\b5432\b/))) {
157
+ const fullBlock = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 10)).join(" ").toLowerCase();
158
+ if (fullBlock.includes("0.0.0.0/0")) {
159
+ findings.push({
160
+ ruleId: "IAC-009",
161
+ severity: "critical",
162
+ category: "infrastructure",
163
+ title: "Database (port 5432) open to the internet",
164
+ description: "Security group allows PostgreSQL access from 0.0.0.0/0. Databases should never be publicly accessible.",
165
+ file: filePath,
166
+ line: i + 1,
167
+ evidence: lines[i].trim(),
168
+ controlIds: ["GDPR-ART32-002", "OWASP-ASVS-006"],
169
+ fix: "Restrict database access to application servers only.",
170
+ });
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ checkDatabases(ctx, findings) {
177
+ for (const [filePath, content] of ctx.fileContents) {
178
+ if (!filePath.endsWith(".tf") && !filePath.endsWith(".yaml") && !filePath.endsWith(".yml"))
179
+ continue;
180
+ if (content.includes("aws_db_instance") || content.includes("aws_rds_cluster") || content.includes("AWS::RDS::DBInstance")) {
181
+ if (content.includes("publicly_accessible") && content.match(/publicly_accessible\s*=\s*true/)) {
182
+ findings.push({
183
+ ruleId: "IAC-010",
184
+ severity: "critical",
185
+ category: "infrastructure",
186
+ title: "RDS instance publicly accessible",
187
+ description: "Database instance is configured as publicly accessible. This exposes the database to the internet.",
188
+ file: filePath,
189
+ evidence: "publicly_accessible = true",
190
+ controlIds: ["GDPR-ART32-002", "OWASP-ASVS-006"],
191
+ fix: "Set publicly_accessible to false. Use VPC-only access.",
192
+ });
193
+ }
194
+ if (!content.includes("storage_encrypted") && !content.includes("StorageEncrypted")) {
195
+ findings.push({
196
+ ruleId: "IAC-011",
197
+ severity: "high",
198
+ category: "encryption",
199
+ title: "RDS instance without encryption at rest",
200
+ description: "Database instance does not have storage encryption enabled. Data at rest is unencrypted.",
201
+ file: filePath,
202
+ evidence: "No storage_encrypted or StorageEncrypted property found",
203
+ controlIds: ["GDPR-ART32-001", "HIPAA-164.312-a"],
204
+ fix: "Set storage_encrypted = true or enable StorageEncrypted.",
205
+ });
206
+ }
207
+ if (!content.includes("deletion_protection") || content.match(/deletion_protection\s*=\s*false/)) {
208
+ findings.push({
209
+ ruleId: "IAC-012",
210
+ severity: "medium",
211
+ category: "infrastructure",
212
+ title: "RDS instance without deletion protection",
213
+ description: "Database instance does not have deletion protection enabled. Accidental deletion can cause data loss.",
214
+ file: filePath,
215
+ evidence: "deletion_protection missing or set to false",
216
+ controlIds: ["GDPR-ART32-008"],
217
+ fix: "Set deletion_protection = true for production databases.",
218
+ });
219
+ }
220
+ }
221
+ }
222
+ }
223
+ checkIAMPolicies(ctx, findings) {
224
+ for (const [filePath, content] of ctx.fileContents) {
225
+ if (!filePath.endsWith(".tf") && !filePath.endsWith(".json") && !filePath.endsWith(".yaml") && !filePath.endsWith(".yml"))
226
+ continue;
227
+ if (content.includes("Action") && content.includes("Resource")) {
228
+ if (content.includes('"*"') && content.match(/Action.*\*/)) {
229
+ findings.push({
230
+ ruleId: "IAC-013",
231
+ severity: "high",
232
+ category: "infrastructure",
233
+ title: "IAM policy with wildcard action",
234
+ description: "IAM policy grants all actions (*). This violates least privilege principle.",
235
+ file: filePath,
236
+ evidence: "Action = * grants full access",
237
+ controlIds: ["OWASP-ASVS-003", "ISO27001-A9", "CIS-005"],
238
+ fix: "Restrict IAM actions to only those required for the role.",
239
+ });
240
+ }
241
+ if (content.match(/Resource.*\*/) && content.includes("arn:aws")) {
242
+ findings.push({
243
+ ruleId: "IAC-014",
244
+ severity: "medium",
245
+ category: "infrastructure",
246
+ title: "IAM policy with wildcard resource",
247
+ description: "IAM policy applies to all resources (*). This is overly permissive.",
248
+ file: filePath,
249
+ evidence: "Resource = * applies to all resources",
250
+ controlIds: ["OWASP-ASVS-003", "ISO27001-A9"],
251
+ fix: "Specify exact resource ARNs instead of using wildcard.",
252
+ });
253
+ }
254
+ }
255
+ }
256
+ }
257
+ checkKMSKeys(ctx, findings) {
258
+ for (const [filePath, content] of ctx.fileContents) {
259
+ if (!filePath.endsWith(".tf"))
260
+ continue;
261
+ if (content.includes("aws_kms_key")) {
262
+ if (content.match(/enable_key_rotation\s*=\s*false/) || !content.includes("enable_key_rotation")) {
263
+ findings.push({
264
+ ruleId: "IAC-015",
265
+ severity: "medium",
266
+ category: "encryption",
267
+ title: "KMS key without rotation",
268
+ description: "KMS key does not have automatic key rotation enabled. Using the same key long-term increases risk.",
269
+ file: filePath,
270
+ evidence: "enable_key_rotation missing or set to false",
271
+ controlIds: ["GDPR-ART32-001", "ISO27001-A10"],
272
+ fix: "Set enable_key_rotation = true on KMS keys.",
273
+ });
274
+ }
275
+ }
276
+ }
277
+ }
278
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greenarmor/ges-audit-engine",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "GESF Audit Engine - Audit trails and compliance evaluation",
6
6
  "main": "./dist/index.js",
@@ -11,16 +11,17 @@
11
11
  "default": "./dist/index.js"
12
12
  }
13
13
  },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
17
+ "test": "vitest run"
18
+ },
14
19
  "dependencies": {
15
- "@greenarmor/ges-core": "1.0.1"
20
+ "@greenarmor/ges-core": "1.1.1"
16
21
  },
17
22
  "devDependencies": {
18
23
  "typescript": "^6.0.0",
19
- "@types/node": "^22.0.0"
20
- },
21
- "scripts": {
22
- "build": "tsc",
23
- "clean": "rm -rf dist tsconfig.tsbuildinfo",
24
- "test": "echo \"no tests yet\""
24
+ "@types/node": "^22.0.0",
25
+ "vitest": "^4.1.8"
25
26
  }
26
- }
27
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025–2026 greenarmor
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.