@rigour-labs/core 3.0.0 → 3.0.2

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.
@@ -46,10 +46,26 @@ export declare class HallucinatedImportsGate extends Gate {
46
46
  private isNodeBuiltin;
47
47
  private isPythonStdlib;
48
48
  /**
49
- * Check Go imports — verify relative/project package paths exist
50
- * Go stdlib packages are skipped; only project-relative imports are checked
49
+ * Check Go imports — verify project-relative package paths exist.
50
+ *
51
+ * Strategy:
52
+ * 1. Skip Go standard library (comprehensive list of 150+ packages)
53
+ * 2. Skip external modules (any path containing a dot → domain name)
54
+ * 3. Parse go.mod for the project module path
55
+ * 4. Only flag imports that match the project module prefix but don't resolve
56
+ *
57
+ * @since v3.0.1 — fixed false positives on Go stdlib (encoding/json, net/http, etc.)
51
58
  */
52
59
  private checkGoImports;
60
+ /**
61
+ * Comprehensive Go standard library package list.
62
+ * Includes all packages from Go 1.22+ (latest stable).
63
+ * Go stdlib is identified by having NO dots in the import path.
64
+ * We maintain an explicit list for packages with slashes (e.g. encoding/json).
65
+ *
66
+ * @since v3.0.1
67
+ */
68
+ private isGoStdlib;
53
69
  /**
54
70
  * Check Ruby imports — verify require_relative paths exist
55
71
  */
@@ -295,12 +295,31 @@ export class HallucinatedImportsGate extends Gate {
295
295
  return stdlibs.has(topLevel);
296
296
  }
297
297
  /**
298
- * Check Go imports — verify relative/project package paths exist
299
- * Go stdlib packages are skipped; only project-relative imports are checked
298
+ * Check Go imports — verify project-relative package paths exist.
299
+ *
300
+ * Strategy:
301
+ * 1. Skip Go standard library (comprehensive list of 150+ packages)
302
+ * 2. Skip external modules (any path containing a dot → domain name)
303
+ * 3. Parse go.mod for the project module path
304
+ * 4. Only flag imports that match the project module prefix but don't resolve
305
+ *
306
+ * @since v3.0.1 — fixed false positives on Go stdlib (encoding/json, net/http, etc.)
300
307
  */
301
308
  checkGoImports(content, file, cwd, projectFiles, hallucinated) {
302
309
  const lines = content.split('\n');
303
310
  let inImportBlock = false;
311
+ // Try to read go.mod for the module path
312
+ const goModPath = path.join(cwd, 'go.mod');
313
+ let modulePath = null;
314
+ try {
315
+ if (fs.pathExistsSync(goModPath)) {
316
+ const goMod = fs.readFileSync(goModPath, 'utf-8');
317
+ const moduleMatch = goMod.match(/^module\s+(\S+)/m);
318
+ if (moduleMatch)
319
+ modulePath = moduleMatch[1];
320
+ }
321
+ }
322
+ catch { /* no go.mod — skip project-relative checks entirely */ }
304
323
  for (let i = 0; i < lines.length; i++) {
305
324
  const line = lines[i].trim();
306
325
  // Detect import block: import ( ... )
@@ -312,30 +331,136 @@ export class HallucinatedImportsGate extends Gate {
312
331
  inImportBlock = false;
313
332
  continue;
314
333
  }
315
- // Single import: import "path"
316
- const singleMatch = line.match(/^import\s+"([^"]+)"/);
317
- const blockMatch = inImportBlock ? line.match(/^\s*"([^"]+)"/) : null;
334
+ // Single import: import "path" or import alias "path"
335
+ const singleMatch = line.match(/^import\s+(?:\w+\s+)?"([^"]+)"/);
336
+ const blockMatch = inImportBlock ? line.match(/^\s*(?:\w+\s+)?"([^"]+)"/) : null;
318
337
  const importPath = singleMatch?.[1] || blockMatch?.[1];
319
338
  if (!importPath)
320
339
  continue;
321
- // Skip Go stdlib (no dots in path = stdlib or well-known)
322
- if (!importPath.includes('.') && !importPath.includes('/'))
340
+ // 1. Skip Go standard library comprehensive list
341
+ if (this.isGoStdlib(importPath))
323
342
  continue;
324
- // Project-relative imports (contain module path from go.mod)
325
- // We only flag imports that look like project paths but don't resolve
326
- if (importPath.includes('/') && !importPath.startsWith('github.com') && !importPath.startsWith('golang.org')) {
327
- // Check if the path maps to a directory in the project
328
- const dirPath = importPath.split('/').slice(-2).join('/');
329
- const hasMatchingFile = [...projectFiles].some(f => f.includes(dirPath));
343
+ // 2. If we have a module path, check project-relative imports FIRST
344
+ // (project imports like github.com/myorg/project/pkg also have dots)
345
+ if (modulePath && importPath.startsWith(modulePath + '/')) {
346
+ const relPath = importPath.slice(modulePath.length + 1);
347
+ const hasMatchingFile = [...projectFiles].some(f => f.endsWith('.go') && f.startsWith(relPath));
330
348
  if (!hasMatchingFile) {
331
349
  hallucinated.push({
332
350
  file, line: i + 1, importPath, type: 'go',
333
- reason: `Go import '${importPath}' — package path not found in project`,
351
+ reason: `Go import '${importPath}' — package directory '${relPath}' not found in project`,
334
352
  });
335
353
  }
354
+ continue;
336
355
  }
356
+ // 3. Skip external modules — any import containing a dot is a domain
357
+ // e.g. github.com/*, google.golang.org/*, go.uber.org/*
358
+ if (importPath.includes('.'))
359
+ continue;
360
+ // 4. No dots, no go.mod match, not stdlib → likely an internal package
361
+ // without go.mod context we can't verify, so skip to avoid false positives
337
362
  }
338
363
  }
364
+ /**
365
+ * Comprehensive Go standard library package list.
366
+ * Includes all packages from Go 1.22+ (latest stable).
367
+ * Go stdlib is identified by having NO dots in the import path.
368
+ * We maintain an explicit list for packages with slashes (e.g. encoding/json).
369
+ *
370
+ * @since v3.0.1
371
+ */
372
+ isGoStdlib(importPath) {
373
+ // Fast check: single-segment packages are always stdlib if no dots
374
+ if (!importPath.includes('/') && !importPath.includes('.'))
375
+ return true;
376
+ // Check the full path against known stdlib packages with sub-paths
377
+ const topLevel = importPath.split('/')[0];
378
+ // All Go stdlib top-level packages (including those with sub-packages)
379
+ const stdlibTopLevel = new Set([
380
+ // Single-word packages
381
+ 'archive', 'bufio', 'builtin', 'bytes', 'cmp', 'compress',
382
+ 'container', 'context', 'crypto', 'database', 'debug',
383
+ 'embed', 'encoding', 'errors', 'expvar', 'flag', 'fmt',
384
+ 'go', 'hash', 'html', 'image', 'index', 'io', 'iter',
385
+ 'log', 'maps', 'math', 'mime', 'net', 'os', 'path',
386
+ 'plugin', 'reflect', 'regexp', 'runtime', 'slices', 'sort',
387
+ 'strconv', 'strings', 'structs', 'sync', 'syscall',
388
+ 'testing', 'text', 'time', 'unicode', 'unique', 'unsafe',
389
+ // Internal packages (used by stdlib, sometimes by tools)
390
+ 'internal', 'vendor',
391
+ ]);
392
+ if (stdlibTopLevel.has(topLevel))
393
+ return true;
394
+ // Explicit full-path list for maximum safety — covers all Go 1.22 stdlib paths
395
+ // This catches any edge case the top-level check might miss
396
+ const knownStdlibPaths = new Set([
397
+ // archive/*
398
+ 'archive/tar', 'archive/zip',
399
+ // compress/*
400
+ 'compress/bzip2', 'compress/flate', 'compress/gzip', 'compress/lzw', 'compress/zlib',
401
+ // container/*
402
+ 'container/heap', 'container/list', 'container/ring',
403
+ // crypto/*
404
+ 'crypto/aes', 'crypto/cipher', 'crypto/des', 'crypto/dsa',
405
+ 'crypto/ecdh', 'crypto/ecdsa', 'crypto/ed25519', 'crypto/elliptic',
406
+ 'crypto/hmac', 'crypto/md5', 'crypto/rand', 'crypto/rc4',
407
+ 'crypto/rsa', 'crypto/sha1', 'crypto/sha256', 'crypto/sha512',
408
+ 'crypto/subtle', 'crypto/tls', 'crypto/x509', 'crypto/x509/pkix',
409
+ // database/*
410
+ 'database/sql', 'database/sql/driver',
411
+ // debug/*
412
+ 'debug/buildinfo', 'debug/dwarf', 'debug/elf', 'debug/gosym',
413
+ 'debug/macho', 'debug/pe', 'debug/plan9obj',
414
+ // encoding/*
415
+ 'encoding/ascii85', 'encoding/asn1', 'encoding/base32', 'encoding/base64',
416
+ 'encoding/binary', 'encoding/csv', 'encoding/gob', 'encoding/hex',
417
+ 'encoding/json', 'encoding/pem', 'encoding/xml',
418
+ // go/*
419
+ 'go/ast', 'go/build', 'go/build/constraint', 'go/constant',
420
+ 'go/doc', 'go/doc/comment', 'go/format', 'go/importer',
421
+ 'go/parser', 'go/printer', 'go/scanner', 'go/token', 'go/types', 'go/version',
422
+ // hash/*
423
+ 'hash/adler32', 'hash/crc32', 'hash/crc64', 'hash/fnv', 'hash/maphash',
424
+ // html/*
425
+ 'html/template',
426
+ // image/*
427
+ 'image/color', 'image/color/palette', 'image/draw',
428
+ 'image/gif', 'image/jpeg', 'image/png',
429
+ // index/*
430
+ 'index/suffixarray',
431
+ // io/*
432
+ 'io/fs', 'io/ioutil',
433
+ // log/*
434
+ 'log/slog', 'log/syslog',
435
+ // math/*
436
+ 'math/big', 'math/bits', 'math/cmplx', 'math/rand', 'math/rand/v2',
437
+ // mime/*
438
+ 'mime/multipart', 'mime/quotedprintable',
439
+ // net/*
440
+ 'net/http', 'net/http/cgi', 'net/http/cookiejar', 'net/http/fcgi',
441
+ 'net/http/httptest', 'net/http/httptrace', 'net/http/httputil',
442
+ 'net/http/pprof', 'net/mail', 'net/netip', 'net/rpc',
443
+ 'net/rpc/jsonrpc', 'net/smtp', 'net/textproto', 'net/url',
444
+ // os/*
445
+ 'os/exec', 'os/signal', 'os/user',
446
+ // path/*
447
+ 'path/filepath',
448
+ // regexp/*
449
+ 'regexp/syntax',
450
+ // runtime/*
451
+ 'runtime/cgo', 'runtime/coverage', 'runtime/debug', 'runtime/metrics',
452
+ 'runtime/pprof', 'runtime/race', 'runtime/trace',
453
+ // sync/*
454
+ 'sync/atomic',
455
+ // testing/*
456
+ 'testing/fstest', 'testing/iotest', 'testing/quick', 'testing/slogtest',
457
+ // text/*
458
+ 'text/scanner', 'text/tabwriter', 'text/template', 'text/template/parse',
459
+ // unicode/*
460
+ 'unicode/utf16', 'unicode/utf8',
461
+ ]);
462
+ return knownStdlibPaths.has(importPath);
463
+ }
339
464
  /**
340
465
  * Check Ruby imports — verify require_relative paths exist
341
466
  */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Hallucinated Imports Gate — Go Standard Library False Positive Regression Tests
3
+ *
4
+ * Tests the fix for https://github.com/rigour-labs/rigour/issues/XXX
5
+ * Previously, Go stdlib packages with slashes (encoding/json, net/http, etc.)
6
+ * were flagged as hallucinated imports because the gate only recognized
7
+ * single-word stdlib packages (fmt, os, io).
8
+ *
9
+ * @since v3.0.1
10
+ */
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+ import { HallucinatedImportsGate } from './hallucinated-imports.js';
13
+ // fs-extra is mocked via module-level mock fns above
14
+ // Mock fs-extra — vi.hoisted ensures these are available when vi.mock runs (hoisted)
15
+ const { mockPathExists, mockPathExistsSync, mockReadFile, mockReadFileSync, mockReadJson } = vi.hoisted(() => ({
16
+ mockPathExists: vi.fn(),
17
+ mockPathExistsSync: vi.fn(),
18
+ mockReadFile: vi.fn(),
19
+ mockReadFileSync: vi.fn(),
20
+ mockReadJson: vi.fn(),
21
+ }));
22
+ vi.mock('fs-extra', () => {
23
+ const mock = {
24
+ pathExists: mockPathExists,
25
+ pathExistsSync: mockPathExistsSync,
26
+ readFile: mockReadFile,
27
+ readFileSync: mockReadFileSync,
28
+ readJson: mockReadJson,
29
+ };
30
+ return {
31
+ ...mock,
32
+ default: mock,
33
+ };
34
+ });
35
+ // Mock FileScanner
36
+ vi.mock('../utils/scanner.js', () => ({
37
+ FileScanner: {
38
+ findFiles: vi.fn().mockResolvedValue([]),
39
+ },
40
+ }));
41
+ import { FileScanner } from '../utils/scanner.js';
42
+ describe('HallucinatedImportsGate — Go stdlib false positives', () => {
43
+ let gate;
44
+ const testCwd = '/tmp/test-go-project';
45
+ const context = {
46
+ cwd: testCwd,
47
+ ignore: [],
48
+ };
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ gate = new HallucinatedImportsGate({ enabled: true });
52
+ });
53
+ /**
54
+ * This is the exact scenario from PicoClaw — Go stdlib packages with slashes
55
+ * were being flagged as hallucinated. These are ALL real Go stdlib packages.
56
+ */
57
+ it('should NOT flag Go standard library packages as hallucinated (PicoClaw regression)', async () => {
58
+ const goFileContent = `package main
59
+
60
+ import (
61
+ "encoding/json"
62
+ "path/filepath"
63
+ "net/http"
64
+ "crypto/rand"
65
+ "crypto/sha256"
66
+ "encoding/base64"
67
+ "os/exec"
68
+ "os/signal"
69
+ "net/url"
70
+ "fmt"
71
+ "io"
72
+ "os"
73
+ "strings"
74
+ "context"
75
+ "sync"
76
+ "time"
77
+ "log"
78
+ "errors"
79
+ "io/ioutil"
80
+ "io/fs"
81
+ "math/rand"
82
+ "regexp"
83
+ "strconv"
84
+ "bytes"
85
+ "bufio"
86
+ "sort"
87
+ "testing"
88
+ "net/http/httptest"
89
+ "database/sql"
90
+ "html/template"
91
+ "text/template"
92
+ "archive/zip"
93
+ "compress/gzip"
94
+ "runtime/debug"
95
+ )
96
+
97
+ func main() {}
98
+ `;
99
+ const goFile = 'main.go';
100
+ FileScanner.findFiles.mockResolvedValue([goFile]);
101
+ mockReadFile.mockResolvedValue(goFileContent);
102
+ mockPathExists.mockResolvedValue(false);
103
+ mockPathExistsSync.mockReturnValue(false); // no go.mod
104
+ const failures = await gate.run(context);
105
+ // ZERO failures — every import above is a real Go stdlib package
106
+ expect(failures).toHaveLength(0);
107
+ });
108
+ it('should NOT flag external module imports (github.com, etc.)', async () => {
109
+ const goFileContent = `package main
110
+
111
+ import (
112
+ "github.com/gin-gonic/gin"
113
+ "github.com/stretchr/testify/assert"
114
+ "google.golang.org/grpc"
115
+ "go.uber.org/zap"
116
+ "golang.org/x/crypto/bcrypt"
117
+ )
118
+
119
+ func main() {}
120
+ `;
121
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
122
+ mockReadFile.mockResolvedValue(goFileContent);
123
+ mockPathExists.mockResolvedValue(false);
124
+ mockPathExistsSync.mockReturnValue(false);
125
+ const failures = await gate.run(context);
126
+ expect(failures).toHaveLength(0);
127
+ });
128
+ it('should flag project-relative imports that do not resolve (with go.mod)', async () => {
129
+ const goMod = `module github.com/myorg/myproject
130
+
131
+ go 1.22
132
+ `;
133
+ const goFileContent = `package main
134
+
135
+ import (
136
+ "fmt"
137
+ "github.com/myorg/myproject/pkg/realmodule"
138
+ "github.com/myorg/myproject/pkg/doesnotexist"
139
+ )
140
+
141
+ func main() {}
142
+ `;
143
+ FileScanner.findFiles.mockResolvedValue(['cmd/main.go', 'pkg/realmodule/handler.go']);
144
+ mockReadFile.mockImplementation(async (filePath) => {
145
+ if (filePath.includes('handler.go'))
146
+ return 'package realmodule\n\nimport "fmt"\n\nfunc Handler() {}\n';
147
+ return goFileContent;
148
+ });
149
+ mockPathExists.mockResolvedValue(false);
150
+ mockPathExistsSync.mockReturnValue(true); // go.mod exists
151
+ mockReadFileSync.mockReturnValue(goMod);
152
+ const failures = await gate.run(context);
153
+ // Should flag doesnotexist but NOT realmodule
154
+ expect(failures).toHaveLength(1);
155
+ expect(failures[0].details).toContain('doesnotexist');
156
+ expect(failures[0].details).not.toContain('realmodule');
157
+ });
158
+ it('should NOT flag anything when no go.mod exists and imports have no dots', async () => {
159
+ // Without go.mod, we can't determine the project module path,
160
+ // so we skip project-relative checks to avoid false positives
161
+ const goFileContent = `package main
162
+
163
+ import (
164
+ "fmt"
165
+ "net/http"
166
+ "internal/custom"
167
+ )
168
+
169
+ func main() {}
170
+ `;
171
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
172
+ mockReadFile.mockResolvedValue(goFileContent);
173
+ mockPathExists.mockResolvedValue(false);
174
+ mockPathExistsSync.mockReturnValue(false);
175
+ const failures = await gate.run(context);
176
+ expect(failures).toHaveLength(0);
177
+ });
178
+ it('should handle single-line imports', async () => {
179
+ const goFileContent = `package main
180
+
181
+ import "fmt"
182
+ import "encoding/json"
183
+ import "net/http"
184
+
185
+ func main() {}
186
+ `;
187
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
188
+ mockReadFile.mockResolvedValue(goFileContent);
189
+ mockPathExists.mockResolvedValue(false);
190
+ mockPathExistsSync.mockReturnValue(false);
191
+ const failures = await gate.run(context);
192
+ expect(failures).toHaveLength(0);
193
+ });
194
+ it('should handle aliased imports', async () => {
195
+ const goFileContent = `package main
196
+
197
+ import (
198
+ "fmt"
199
+ mrand "math/rand"
200
+ _ "net/http/pprof"
201
+ . "os"
202
+ )
203
+
204
+ func main() {}
205
+ `;
206
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
207
+ mockReadFile.mockResolvedValue(goFileContent);
208
+ mockPathExists.mockResolvedValue(false);
209
+ mockPathExistsSync.mockReturnValue(false);
210
+ const failures = await gate.run(context);
211
+ expect(failures).toHaveLength(0);
212
+ });
213
+ });
214
+ describe('HallucinatedImportsGate — Python stdlib coverage', () => {
215
+ let gate;
216
+ const testCwd = '/tmp/test-py-project';
217
+ const context = {
218
+ cwd: testCwd,
219
+ ignore: [],
220
+ };
221
+ beforeEach(() => {
222
+ vi.clearAllMocks();
223
+ gate = new HallucinatedImportsGate({ enabled: true });
224
+ });
225
+ it('should NOT flag Python standard library imports', async () => {
226
+ const pyContent = `
227
+ import os
228
+ import sys
229
+ import json
230
+ import hashlib
231
+ import pathlib
232
+ import subprocess
233
+ import argparse
234
+ import typing
235
+ import dataclasses
236
+ import functools
237
+ import itertools
238
+ import collections
239
+ import datetime
240
+ import re
241
+ import math
242
+ import random
243
+ import threading
244
+ import asyncio
245
+ from os.path import join, exists
246
+ from collections import defaultdict
247
+ from typing import List, Optional
248
+ from urllib.parse import urlparse
249
+ `;
250
+ FileScanner.findFiles.mockResolvedValue(['main.py']);
251
+ mockReadFile.mockResolvedValue(pyContent);
252
+ mockPathExists.mockResolvedValue(false);
253
+ const failures = await gate.run(context);
254
+ expect(failures).toHaveLength(0);
255
+ });
256
+ });
257
+ describe('HallucinatedImportsGate — JS/TS Node builtins', () => {
258
+ let gate;
259
+ const testCwd = '/tmp/test-node-project';
260
+ const context = {
261
+ cwd: testCwd,
262
+ ignore: [],
263
+ };
264
+ beforeEach(() => {
265
+ vi.clearAllMocks();
266
+ gate = new HallucinatedImportsGate({ enabled: true });
267
+ });
268
+ it('should NOT flag Node.js built-in modules', async () => {
269
+ const jsContent = `
270
+ import fs from 'fs';
271
+ import path from 'path';
272
+ import crypto from 'crypto';
273
+ import http from 'http';
274
+ import https from 'https';
275
+ import url from 'url';
276
+ import os from 'os';
277
+ import stream from 'stream';
278
+ import util from 'util';
279
+ import { readFile } from 'node:fs';
280
+ import { join } from 'node:path';
281
+ `;
282
+ FileScanner.findFiles.mockResolvedValue(['index.ts']);
283
+ mockReadFile.mockResolvedValue(jsContent);
284
+ mockPathExists.mockResolvedValue(false);
285
+ const failures = await gate.run(context);
286
+ expect(failures).toHaveLength(0);
287
+ });
288
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Tests for OWASP-aligned security patterns added in v3.0.0.
3
+ * Covers: ReDoS, overly permissive code, unsafe output, missing input validation.
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { SecurityPatternsGate, checkSecurityPatterns } from './security-patterns.js';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ describe('SecurityPatternsGate — OWASP extended patterns', () => {
11
+ let testDir;
12
+ beforeEach(() => {
13
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owasp-test-'));
14
+ });
15
+ afterEach(() => {
16
+ fs.rmSync(testDir, { recursive: true, force: true });
17
+ });
18
+ describe('ReDoS detection (OWASP #7)', () => {
19
+ it('should detect dynamic regex from user input', async () => {
20
+ const filePath = path.join(testDir, 'search.ts');
21
+ fs.writeFileSync(filePath, `
22
+ const pattern = new RegExp(req.query.search);
23
+ const matches = text.match(pattern);
24
+ `);
25
+ const vulns = await checkSecurityPatterns(filePath);
26
+ expect(vulns.some(v => v.type === 'redos')).toBe(true);
27
+ });
28
+ it('should detect nested quantifiers', async () => {
29
+ const filePath = path.join(testDir, 'regex.ts');
30
+ fs.writeFileSync(filePath, `
31
+ const re = /(?:a+)+b/;
32
+ `);
33
+ const vulns = await checkSecurityPatterns(filePath);
34
+ expect(vulns.some(v => v.type === 'redos')).toBe(true);
35
+ });
36
+ it('should allow safe regex patterns', async () => {
37
+ const filePath = path.join(testDir, 'safe-regex.ts');
38
+ fs.writeFileSync(filePath, `
39
+ const re = /^[a-z]+$/;
40
+ `);
41
+ const vulns = await checkSecurityPatterns(filePath);
42
+ expect(vulns.filter(v => v.type === 'redos')).toHaveLength(0);
43
+ });
44
+ });
45
+ describe('Overly Permissive Code (OWASP #9)', () => {
46
+ it('should detect CORS wildcard origin', async () => {
47
+ const filePath = path.join(testDir, 'server.ts');
48
+ fs.writeFileSync(filePath, `
49
+ import cors from 'cors';
50
+ app.use(cors({ origin: '*' }));
51
+ `);
52
+ const vulns = await checkSecurityPatterns(filePath);
53
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
54
+ });
55
+ it('should detect CORS origin true', async () => {
56
+ const filePath = path.join(testDir, 'server2.ts');
57
+ fs.writeFileSync(filePath, `
58
+ app.use(cors({ origin: true }));
59
+ `);
60
+ const vulns = await checkSecurityPatterns(filePath);
61
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
62
+ });
63
+ it('should detect 0.0.0.0 binding', async () => {
64
+ const filePath = path.join(testDir, 'listen.ts');
65
+ fs.writeFileSync(filePath, `
66
+ app.listen(3000, '0.0.0.0');
67
+ `);
68
+ const vulns = await checkSecurityPatterns(filePath);
69
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
70
+ });
71
+ it('should detect chmod 777', async () => {
72
+ const filePath = path.join(testDir, 'perms.ts');
73
+ fs.writeFileSync(filePath, `
74
+ fs.chmod('/tmp/data', 0o777);
75
+ `);
76
+ const vulns = await checkSecurityPatterns(filePath);
77
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
78
+ });
79
+ it('should detect wildcard CORS header', async () => {
80
+ const filePath = path.join(testDir, 'headers.ts');
81
+ fs.writeFileSync(filePath, `
82
+ res.setHeader('Access-Control-Allow-Origin', '*');
83
+ `);
84
+ const vulns = await checkSecurityPatterns(filePath);
85
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
86
+ });
87
+ it('should allow specific CORS origin', async () => {
88
+ const filePath = path.join(testDir, 'safe-cors.ts');
89
+ fs.writeFileSync(filePath, `
90
+ app.use(cors({ origin: 'https://myapp.com' }));
91
+ `);
92
+ const vulns = await checkSecurityPatterns(filePath);
93
+ expect(vulns.filter(v => v.type === 'overly_permissive')).toHaveLength(0);
94
+ });
95
+ });
96
+ describe('Unsafe Output Handling (OWASP #6)', () => {
97
+ it('should detect response reflecting user input', async () => {
98
+ const filePath = path.join(testDir, 'handler.ts');
99
+ fs.writeFileSync(filePath, `
100
+ app.get('/echo', (req, res) => {
101
+ res.send(req.query.msg);
102
+ });
103
+ `);
104
+ const vulns = await checkSecurityPatterns(filePath);
105
+ expect(vulns.some(v => v.type === 'unsafe_output')).toBe(true);
106
+ });
107
+ it('should detect eval with user input', async () => {
108
+ const filePath = path.join(testDir, 'eval.ts');
109
+ fs.writeFileSync(filePath, `
110
+ eval(req.body.code);
111
+ `);
112
+ const vulns = await checkSecurityPatterns(filePath);
113
+ expect(vulns.some(v => v.type === 'unsafe_output')).toBe(true);
114
+ });
115
+ it('should allow safe response patterns', async () => {
116
+ const filePath = path.join(testDir, 'safe-res.ts');
117
+ fs.writeFileSync(filePath, `
118
+ res.json({ status: 'ok', data: processedData });
119
+ `);
120
+ const vulns = await checkSecurityPatterns(filePath);
121
+ expect(vulns.filter(v => v.type === 'unsafe_output')).toHaveLength(0);
122
+ });
123
+ });
124
+ describe('Missing Input Validation (OWASP #8)', () => {
125
+ it('should detect JSON.parse on raw body', async () => {
126
+ const filePath = path.join(testDir, 'parse.ts');
127
+ fs.writeFileSync(filePath, `
128
+ const data = JSON.parse(req.body);
129
+ `);
130
+ const vulns = await checkSecurityPatterns(filePath);
131
+ expect(vulns.some(v => v.type === 'missing_input_validation')).toBe(true);
132
+ });
133
+ it('should detect "as any" type assertion', async () => {
134
+ const filePath = path.join(testDir, 'assert.ts');
135
+ fs.writeFileSync(filePath, `
136
+ const user = payload as any;
137
+ `);
138
+ const vulns = await checkSecurityPatterns(filePath);
139
+ expect(vulns.some(v => v.type === 'missing_input_validation')).toBe(true);
140
+ });
141
+ it('should allow validated JSON parse', async () => {
142
+ const filePath = path.join(testDir, 'safe-parse.ts');
143
+ fs.writeFileSync(filePath, `
144
+ const data = JSON.parse(rawString);
145
+ const validated = schema.parse(data);
146
+ `);
147
+ const vulns = await checkSecurityPatterns(filePath);
148
+ expect(vulns.filter(v => v.type === 'missing_input_validation')).toHaveLength(0);
149
+ });
150
+ });
151
+ describe('config toggles for new patterns', () => {
152
+ it('should disable redos when configured', async () => {
153
+ const gate = new SecurityPatternsGate({ enabled: true, redos: false });
154
+ const filePath = path.join(testDir, 'regex.ts');
155
+ fs.writeFileSync(filePath, `
156
+ const pattern = new RegExp(req.query.search);
157
+ `);
158
+ const failures = await gate.run({ cwd: testDir });
159
+ expect(failures.filter(f => f.title?.includes('ReDoS') || f.title?.includes('regex'))).toHaveLength(0);
160
+ });
161
+ it('should disable overly_permissive when configured', async () => {
162
+ const gate = new SecurityPatternsGate({ enabled: true, overly_permissive: false });
163
+ const filePath = path.join(testDir, 'cors.ts');
164
+ fs.writeFileSync(filePath, `
165
+ app.use(cors({ origin: '*' }));
166
+ `);
167
+ const failures = await gate.run({ cwd: testDir });
168
+ expect(failures.filter(f => f.title?.includes('CORS') || f.title?.includes('permissive'))).toHaveLength(0);
169
+ });
170
+ });
171
+ });