@greenarmor/ges-audit-engine 0.4.0 → 0.5.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.js CHANGED
@@ -10,7 +10,11 @@ const IGNORE_DIRS = new Set([
10
10
  "node_modules", ".git", "dist", "build", ".next", ".nuxt", "coverage",
11
11
  ".ges", "vendor", "__pycache__", ".venv", "venv", ".turbo", ".cache",
12
12
  "reports", "compliance", "security", "controls", "policies", "checklists", "docs",
13
+ "bundle", ".crush", ".vscode", ".idea",
13
14
  ]);
15
+ const SKIP_PATHS = [
16
+ "/audit-engine/src/",
17
+ ];
14
18
  const IGNORE_EXTENSIONS = new Set([
15
19
  ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".woff",
16
20
  ".woff2", ".ttf", ".eot", ".mp4", ".mp3", ".zip", ".gz", ".tar",
@@ -30,7 +34,10 @@ function collectFiles(root) {
30
34
  else if (entry.isFile()) {
31
35
  const ext = path.extname(entry.name);
32
36
  if (!IGNORE_EXTENSIONS.has(ext)) {
33
- files.push(path.relative(root, fullPath));
37
+ const rel = path.relative(root, fullPath).replace(/\\/g, "/");
38
+ if (!SKIP_PATHS.some(skip => rel.includes(skip))) {
39
+ files.push(rel);
40
+ }
34
41
  }
35
42
  }
36
43
  }
@@ -56,10 +63,85 @@ function readFiles(root, files) {
56
63
  }
57
64
  return contents;
58
65
  }
66
+ function detectWebProject(fileContents) {
67
+ const webPatterns = [
68
+ /from\s+['"]express['"]/, /require\s*\(\s*['"]express['"]\s*\)/,
69
+ /from\s+['"]fastify['"]/, /require\s*\(\s*['"]fastify['"]\s*\)/,
70
+ /from\s+['"]koa['"]/, /require\s*\(\s*['"]koa['"]\s*\)/,
71
+ /from\s+['"]hono['"]/, /require\s*\(\s*['"]hono['"]\s*\)/,
72
+ /from\s+['"]@nestjs/, /from\s+['"]next['"]/, /from\s+['"]nuxt['"]/,
73
+ /from\s+['"]@sveltejs/, /from\s+['"]@remix-run/,
74
+ /from\s+['"]@angular/, /from\s+['"]vue['"]/,
75
+ /import\s+django/, /from\s+flask\s+import/, /from\s+fastapi\s+import/,
76
+ /from\s+sanic\s+import/, /from\s+aiohttp\s+import/, /import\s+tornado/,
77
+ /use\s+gin\.Default\(\)|gin\.New\(\)/, /fiber\.New\(\)/,
78
+ /echo\.New\(\)/, /mux\.NewRouter\(\)/, /chi\.NewRouter\(\)/,
79
+ /iris\.New\(\)/,
80
+ /use\s+Actix\s*Web/, /use\s+rocket/, /use\s+warp/, /use\s+axum/,
81
+ /Rails\.application/, /ActionController::Base/, /Sinatra::Base/,
82
+ /import\s+io\.express/, /import\s+io\.ktor/, /import\s+spark\.Spark/,
83
+ /@SpringBootApplication/, /@Controller/, /@RestController/,
84
+ /use\s+Rocketeer/, /Route::get|Route::post/, /use\s+Illuminate/,
85
+ /using\s+Microsoft\.AspNetCore/, /using\s+Nancy/, /ControllerBase/,
86
+ /createServer\s*\(\s*.*request\b/, /http\.createServer/,
87
+ /router\.(get|post|put|delete|patch)\s*\(/,
88
+ ];
89
+ for (const [, content] of fileContents) {
90
+ for (const pattern of webPatterns) {
91
+ if (pattern.test(content))
92
+ return true;
93
+ }
94
+ }
95
+ for (const [filePath, content] of fileContents) {
96
+ if (filePath === "package.json") {
97
+ try {
98
+ const pkg = JSON.parse(content);
99
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
100
+ if (allDeps.express || allDeps.fastify || allDeps.koa || allDeps.hono ||
101
+ allDeps.next || allDeps.nuxt || allDeps["@nestjs/core"] || allDeps["@sveltejs/kit"] ||
102
+ allDeps["@remix-run/node"] || allDeps["@angular/core"] || allDeps.vue) {
103
+ return true;
104
+ }
105
+ }
106
+ catch { /* not json */ }
107
+ }
108
+ if (filePath === "requirements.txt" || filePath === "pyproject.toml") {
109
+ if (/django|flask|fastapi|sanic|aiohttp|tornado|starlette/i.test(content))
110
+ return true;
111
+ }
112
+ if (filePath === "go.mod") {
113
+ if (/gin-gonic|fiber|echo|chi|gorilla\/mux|iris/i.test(content))
114
+ return true;
115
+ }
116
+ if (filePath === "Cargo.toml") {
117
+ if (/actix-web|rocket|warp|axum|tide/i.test(content))
118
+ return true;
119
+ }
120
+ if (filePath === "Gemfile") {
121
+ if (/rails|sinatra|hanami/i.test(content))
122
+ return true;
123
+ }
124
+ if (filePath === "pom.xml" || filePath === "build.gradle") {
125
+ if (/spring-boot|ktor|sparkjava|quarkus/i.test(content))
126
+ return true;
127
+ }
128
+ if (filePath === "composer.json") {
129
+ try {
130
+ const pkg = JSON.parse(content);
131
+ const allDeps = { ...pkg.require, ...pkg["require-dev"] };
132
+ if (allDeps["laravel/framework"] || allDeps["symfony/symfony"] || allDeps["slim/slim"])
133
+ return true;
134
+ }
135
+ catch { /* not json */ }
136
+ }
137
+ }
138
+ return false;
139
+ }
59
140
  export function runAudit(root) {
60
141
  const files = collectFiles(root);
61
142
  const fileContents = readFiles(root, files);
62
- const ctx = { root, files, fileContents };
143
+ const isWebProject = detectWebProject(fileContents);
144
+ const ctx = { root, files, fileContents, isWebProject };
63
145
  const scanners = [
64
146
  new SecretsScanner(),
65
147
  new CryptoScanner(),
@@ -4,6 +4,8 @@ export class AuthScanner {
4
4
  scan(ctx) {
5
5
  const findings = [];
6
6
  const content = ctx.fileContents;
7
+ if (!ctx.isWebProject)
8
+ return findings;
7
9
  const hasAuthMiddleware = this.detectAuthMiddleware(content);
8
10
  const routesWithoutAuth = this.detectRoutesWithoutAuth(content, hasAuthMiddleware);
9
11
  const hasRateLimiting = this.detectRateLimiting(content);
@@ -43,7 +43,7 @@ export class ConfigScanner {
43
43
  fix: "npm install cors and configure allowed origins explicitly.",
44
44
  });
45
45
  }
46
- const auditDeps = ["express", "lodash", "axios", "underscore"];
46
+ const auditDeps = ["lodash", "axios", "underscore"];
47
47
  for (const dep of auditDeps) {
48
48
  if (deps[dep]) {
49
49
  findings.push({
@@ -1,3 +1,39 @@
1
+ const DB_SCHEMA_EXTENSIONS = new Set([
2
+ ".prisma", ".sql",
3
+ ]);
4
+ const DB_SCHEMA_FILENAMES = [
5
+ /(?:^|[\/\\])(?:schema|migration|knexfile|drizzle\.config|database\.conf)/i,
6
+ ];
7
+ const DB_DIR_INDICATORS = [
8
+ /[\/\\](?:migrations?|models?|entities?|repositories?|schemas?|db|database)[\/\\]/i,
9
+ ];
10
+ const ORM_ENTITY_PATTERNS = {
11
+ ".ts": /(?:@Entity|@Table|@Schema|BaseModel)\s*\(|(?:Model|Entity|Schema)\s+extends\s+|Schema\s*=\s*new\s+mongoose\.Schema/i,
12
+ ".js": /(?:@Entity|@Table|@Schema|BaseModel)\s*\(/,
13
+ ".py": /class\s+\w+\s*\(\s*(?:models\.Model|Base|declarative_base)\)/i,
14
+ ".rb": /class\s+\w+\s*<\s*(?:ApplicationRecord|ActiveRecord::Base)/i,
15
+ ".go": /type\s+\w+\s+struct\s*\{[\s\S]*?gorm/i,
16
+ ".java": /@Entity\s*(?:public\s+)?class/i,
17
+ ".php": /class\s+\w+\s+extends\s+(?:Model|Eloquent|Doctrine)/i,
18
+ };
19
+ function isDatabaseSchemaFile(filePath, content) {
20
+ const ext = filePath.substring(filePath.lastIndexOf("."));
21
+ const basename = filePath.substring(filePath.lastIndexOf("/") + 1);
22
+ if (DB_SCHEMA_EXTENSIONS.has(ext))
23
+ return true;
24
+ for (const pattern of DB_SCHEMA_FILENAMES) {
25
+ if (pattern.test(basename))
26
+ return true;
27
+ }
28
+ for (const pattern of DB_DIR_INDICATORS) {
29
+ if (pattern.test(filePath))
30
+ return true;
31
+ }
32
+ const ormPattern = ORM_ENTITY_PATTERNS[ext];
33
+ if (ormPattern && ormPattern.test(content))
34
+ return true;
35
+ return false;
36
+ }
1
37
  export class DatabaseScanner {
2
38
  name = "database";
3
39
  scan(ctx) {
@@ -7,56 +43,55 @@ export class DatabaseScanner {
7
43
  return findings;
8
44
  }
9
45
  checkSchemaPatterns(ctx, findings) {
10
- const requiredAuditColumns = ["created_at", "updated_at"];
11
- const recommendedAuditColumns = ["deleted_at", "created_by", "updated_by"];
12
- const codeExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".prisma", ".sql"]);
13
46
  for (const [filePath, content] of ctx.fileContents) {
14
- const ext = filePath.substring(filePath.lastIndexOf("."));
15
- if (!codeExtensions.has(ext))
47
+ if (!isDatabaseSchemaFile(filePath, content))
16
48
  continue;
17
- const hasTimestamps = /\b(?:timestamps|created_at|createdAt)\s*[:\(]/i.test(content);
18
- const hasSoftDelete = /\b(?:deleted_at|deletedAt|softDelete|paranoid)\s*[:\(]/i.test(content);
19
- const hasUserAudit = /\b(?:created_by|createdBy|updated_by|updatedBy)\s*[:\(]/i.test(content);
20
- if (/\b(?:model|schema|entity|table)\b.*\{/i.test(content) || /\bCREATE\s+TABLE\b/i.test(content)) {
21
- if (!hasTimestamps) {
22
- findings.push({
23
- ruleId: "DB-001",
24
- severity: "high",
25
- category: "database",
26
- title: "Missing audit timestamps in schema",
27
- description: "Database schema does not include created_at/updated_at timestamps. These are mandatory for audit trails.",
28
- file: filePath,
29
- evidence: "No created_at/updated_at columns detected",
30
- controlIds: ["GDPR-ART32-006"],
31
- fix: "Add created_at and updated_at columns to all tables. In Prisma: @@map, in Sequelize: timestamps: true.",
32
- });
33
- }
34
- if (!hasSoftDelete) {
35
- findings.push({
36
- ruleId: "DB-002",
37
- severity: "medium",
38
- category: "database",
39
- title: "Missing soft delete pattern",
40
- description: "No deleted_at column or soft delete pattern found. Hard deletes prevent audit trail and data recovery.",
41
- file: filePath,
42
- evidence: "No deleted_at/softDelete pattern detected",
43
- controlIds: ["GDPR-ART32-007"],
44
- fix: "Add deleted_at column. In Prisma: add DeletedAt DateTime?, in Sequelize: paranoid: true.",
45
- });
46
- }
47
- if (!hasUserAudit) {
48
- findings.push({
49
- ruleId: "DB-003",
50
- severity: "medium",
51
- category: "database",
52
- title: "Missing user audit columns",
53
- description: "No created_by/updated_by columns found. Track who makes changes for accountability.",
54
- file: filePath,
55
- evidence: "No created_by/updated_by columns detected",
56
- controlIds: ["GDPR-ART32-006"],
57
- fix: "Add created_by and updated_by columns to track which user made changes.",
58
- });
59
- }
49
+ const hasTimestamps = /\b(?:timestamps|created_at|createdAt|createdDate|date_created|timecreated|createdTime)\s*[:\(]/i.test(content);
50
+ const hasSoftDelete = /\b(?:deleted_at|deletedAt|softDelete|paranoid|is_deleted|isDeleted|deleted|active)\s*[:\(]/i.test(content);
51
+ const hasUserAudit = /\b(?:created_by|createdBy|updated_by|updatedBy|owner_id|author_id)\s*[:\(]/i.test(content);
52
+ const hasSchemaDef = /\b(?:model|schema|entity|table|struct|class)\b.*\{/i.test(content) ||
53
+ /\bCREATE\s+TABLE\b/i.test(content) ||
54
+ /@(?:Entity|Table|Schema)\b/.test(content);
55
+ if (!hasSchemaDef)
56
+ continue;
57
+ if (!hasTimestamps) {
58
+ findings.push({
59
+ ruleId: "DB-001",
60
+ severity: "high",
61
+ category: "database",
62
+ title: "Missing audit timestamps in schema",
63
+ description: "Database schema does not include created_at/updated_at timestamps. These are mandatory for audit trails.",
64
+ file: filePath,
65
+ evidence: "No created_at/updated_at columns detected",
66
+ controlIds: ["GDPR-ART32-006"],
67
+ fix: "Add created_at and updated_at columns. In Prisma: add DateTime fields, in Sequelize: timestamps: true, in Django: auto_now_add=True.",
68
+ });
69
+ }
70
+ if (!hasSoftDelete) {
71
+ findings.push({
72
+ ruleId: "DB-002",
73
+ severity: "medium",
74
+ category: "database",
75
+ title: "Missing soft delete pattern",
76
+ description: "No deleted_at column or soft delete pattern found. Hard deletes prevent audit trail and data recovery.",
77
+ file: filePath,
78
+ evidence: "No deleted_at/softDelete pattern detected",
79
+ controlIds: ["GDPR-ART32-007"],
80
+ fix: "Add deleted_at column or soft delete flag. In Prisma: DeletedAt DateTime?, in Sequelize: paranoid: true, in Django: SoftDeleteModel.",
81
+ });
82
+ }
83
+ if (!hasUserAudit) {
84
+ findings.push({
85
+ ruleId: "DB-003",
86
+ severity: "medium",
87
+ category: "database",
88
+ title: "Missing user audit columns",
89
+ description: "No created_by/updated_by columns found. Track who makes changes for accountability.",
90
+ file: filePath,
91
+ evidence: "No created_by/updated_by columns detected",
92
+ controlIds: ["GDPR-ART32-006"],
93
+ fix: "Add created_by and updated_by columns to track which user made changes.",
94
+ });
60
95
  }
61
96
  }
62
97
  }
@@ -15,6 +15,7 @@ export interface ScanContext {
15
15
  files: string[];
16
16
  fileContents: Map<string, string>;
17
17
  config?: Record<string, unknown>;
18
+ isWebProject?: boolean;
18
19
  }
19
20
  export interface Scanner {
20
21
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greenarmor/ges-audit-engine",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "GESF Audit Engine - Audit trails and compliance evaluation",
6
6
  "main": "./dist/index.js",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@greenarmor/ges-core": "0.4.0"
15
+ "@greenarmor/ges-core": "0.5.1"
16
16
  },
17
17
  "devDependencies": {
18
18
  "typescript": "^6.0.0",