@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.
- package/dist/gates/hallucinated-imports.d.ts +18 -2
- package/dist/gates/hallucinated-imports.js +139 -14
- package/dist/gates/hallucinated-imports.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports.test.js +288 -0
- package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
- package/dist/gates/security-patterns-owasp.test.js +171 -0
- package/dist/gates/security-patterns.d.ts +4 -0
- package/dist/gates/security-patterns.js +100 -0
- package/dist/hooks/checker.d.ts +23 -0
- package/dist/hooks/checker.js +222 -0
- package/dist/hooks/checker.test.d.ts +1 -0
- package/dist/hooks/checker.test.js +132 -0
- package/dist/hooks/index.d.ts +9 -0
- package/dist/hooks/index.js +8 -0
- package/dist/hooks/standalone-checker.d.ts +15 -0
- package/dist/hooks/standalone-checker.js +106 -0
- package/dist/hooks/templates.d.ts +22 -0
- package/dist/hooks/templates.js +232 -0
- package/dist/hooks/types.d.ts +34 -0
- package/dist/hooks/types.js +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/types/index.d.ts +54 -0
- package/dist/types/index.js +13 -0
- package/package.json +1 -1
|
@@ -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
|
|
50
|
-
*
|
|
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
|
|
299
|
-
*
|
|
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
|
|
322
|
-
if (
|
|
340
|
+
// 1. Skip Go standard library — comprehensive list
|
|
341
|
+
if (this.isGoStdlib(importPath))
|
|
323
342
|
continue;
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
const
|
|
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
|
|
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
|
+
});
|