@rigour-labs/core 3.0.3 → 3.0.5
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/deprecated-apis.d.ts +55 -0
- package/dist/gates/deprecated-apis.js +724 -0
- package/dist/gates/deprecated-apis.test.d.ts +1 -0
- package/dist/gates/deprecated-apis.test.js +288 -0
- package/dist/gates/phantom-apis.d.ts +77 -0
- package/dist/gates/phantom-apis.js +675 -0
- package/dist/gates/phantom-apis.test.d.ts +1 -0
- package/dist/gates/phantom-apis.test.js +320 -0
- package/dist/gates/runner.js +13 -0
- package/dist/gates/test-quality.d.ts +67 -0
- package/dist/gates/test-quality.js +512 -0
- package/dist/gates/test-quality.test.d.ts +1 -0
- package/dist/gates/test-quality.test.js +312 -0
- package/dist/templates/index.js +31 -1
- package/dist/types/index.d.ts +348 -0
- package/dist/types/index.js +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
const mockFindFiles = vi.hoisted(() => vi.fn());
|
|
3
|
+
const mockReadFile = vi.hoisted(() => vi.fn());
|
|
4
|
+
vi.mock('../utils/scanner.js', () => ({
|
|
5
|
+
FileScanner: { findFiles: mockFindFiles },
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('fs-extra', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
readFile: mockReadFile,
|
|
10
|
+
pathExists: vi.fn().mockResolvedValue(false),
|
|
11
|
+
pathExistsSync: vi.fn().mockReturnValue(false),
|
|
12
|
+
readFileSync: vi.fn().mockReturnValue(''),
|
|
13
|
+
readJson: vi.fn().mockResolvedValue(null),
|
|
14
|
+
readdirSync: vi.fn().mockReturnValue([]),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
import { DeprecatedApisGate } from './deprecated-apis.js';
|
|
18
|
+
describe('DeprecatedApisGate — Node.js Security', () => {
|
|
19
|
+
let gate;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
gate = new DeprecatedApisGate();
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
it('should flag new Buffer() as security-critical', async () => {
|
|
25
|
+
mockFindFiles.mockResolvedValue(['src/handler.js']);
|
|
26
|
+
mockReadFile.mockResolvedValue(`
|
|
27
|
+
const buf = new Buffer(100);
|
|
28
|
+
const buf2 = new Buffer('hello');
|
|
29
|
+
`);
|
|
30
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
31
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
32
|
+
const secFail = failures.find(f => f.title === 'Security-Deprecated APIs');
|
|
33
|
+
expect(secFail).toBeDefined();
|
|
34
|
+
expect(secFail.severity).toBe('critical');
|
|
35
|
+
expect(secFail.details).toContain('Buffer');
|
|
36
|
+
});
|
|
37
|
+
it('should flag crypto.createCipher as security-critical', async () => {
|
|
38
|
+
mockFindFiles.mockResolvedValue(['src/encrypt.ts']);
|
|
39
|
+
mockReadFile.mockResolvedValue(`
|
|
40
|
+
import crypto from 'crypto';
|
|
41
|
+
const cipher = crypto.createCipher('aes-256-cbc', 'password');
|
|
42
|
+
`);
|
|
43
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
44
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
45
|
+
expect(failures[0].details).toContain('createCipher');
|
|
46
|
+
});
|
|
47
|
+
it('should flag document.write() as security risk', async () => {
|
|
48
|
+
mockFindFiles.mockResolvedValue(['src/page.tsx']);
|
|
49
|
+
mockReadFile.mockResolvedValue(`
|
|
50
|
+
document.write('<script>alert("xss")</script>');
|
|
51
|
+
`);
|
|
52
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
53
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
54
|
+
expect(failures[0].details).toContain('document.write');
|
|
55
|
+
});
|
|
56
|
+
it('should flag eval() as security risk', async () => {
|
|
57
|
+
mockFindFiles.mockResolvedValue(['src/dynamic.js']);
|
|
58
|
+
mockReadFile.mockResolvedValue(`
|
|
59
|
+
const result = eval(userInput);
|
|
60
|
+
`);
|
|
61
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
62
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
63
|
+
expect(failures[0].details).toContain('eval');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('DeprecatedApisGate — Node.js Superseded', () => {
|
|
67
|
+
let gate;
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
gate = new DeprecatedApisGate();
|
|
70
|
+
vi.clearAllMocks();
|
|
71
|
+
});
|
|
72
|
+
it('should flag url.parse() as superseded', async () => {
|
|
73
|
+
mockFindFiles.mockResolvedValue(['src/router.ts']);
|
|
74
|
+
mockReadFile.mockResolvedValue(`
|
|
75
|
+
import url from 'url';
|
|
76
|
+
const parsed = url.parse(req.url);
|
|
77
|
+
`);
|
|
78
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
79
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
80
|
+
expect(failures[0].details).toContain('url.parse');
|
|
81
|
+
});
|
|
82
|
+
it('should flag fs.exists() as superseded', async () => {
|
|
83
|
+
mockFindFiles.mockResolvedValue(['src/checker.ts']);
|
|
84
|
+
mockReadFile.mockResolvedValue(`
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
fs.exists('/tmp/test', (exists) => {});
|
|
87
|
+
`);
|
|
88
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
89
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
90
|
+
expect(failures[0].details).toContain('fs.exists');
|
|
91
|
+
});
|
|
92
|
+
it('should flag require("domain") as removed', async () => {
|
|
93
|
+
mockFindFiles.mockResolvedValue(['src/app.js']);
|
|
94
|
+
mockReadFile.mockResolvedValue(`
|
|
95
|
+
const domain = require('domain');
|
|
96
|
+
`);
|
|
97
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
98
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
99
|
+
expect(failures[0].details).toContain('domain');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('DeprecatedApisGate — Python', () => {
|
|
103
|
+
let gate;
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
gate = new DeprecatedApisGate();
|
|
106
|
+
vi.clearAllMocks();
|
|
107
|
+
});
|
|
108
|
+
it('should flag pickle.loads() as security risk', async () => {
|
|
109
|
+
mockFindFiles.mockResolvedValue(['handler.py']);
|
|
110
|
+
mockReadFile.mockResolvedValue(`
|
|
111
|
+
import pickle
|
|
112
|
+
data = pickle.loads(untrusted_input)
|
|
113
|
+
`);
|
|
114
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
115
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
116
|
+
expect(failures[0].details).toContain('pickle');
|
|
117
|
+
});
|
|
118
|
+
it('should flag os.system() as security risk', async () => {
|
|
119
|
+
mockFindFiles.mockResolvedValue(['runner.py']);
|
|
120
|
+
mockReadFile.mockResolvedValue(`
|
|
121
|
+
import os
|
|
122
|
+
os.system(user_command)
|
|
123
|
+
`);
|
|
124
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
125
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
126
|
+
expect(failures[0].details).toContain('os.system');
|
|
127
|
+
});
|
|
128
|
+
it('should flag subprocess with shell=True', async () => {
|
|
129
|
+
mockFindFiles.mockResolvedValue(['runner.py']);
|
|
130
|
+
mockReadFile.mockResolvedValue(`
|
|
131
|
+
import subprocess
|
|
132
|
+
subprocess.run(cmd, shell=True)
|
|
133
|
+
`);
|
|
134
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
135
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
136
|
+
expect(failures[0].details).toContain('shell=True');
|
|
137
|
+
});
|
|
138
|
+
it('should flag import imp as removed', async () => {
|
|
139
|
+
mockFindFiles.mockResolvedValue(['loader.py']);
|
|
140
|
+
mockReadFile.mockResolvedValue(`
|
|
141
|
+
import imp
|
|
142
|
+
module = imp.load_source('name', 'path')
|
|
143
|
+
`);
|
|
144
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
145
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
146
|
+
expect(failures[0].details).toContain('imp');
|
|
147
|
+
});
|
|
148
|
+
it('should flag from distutils as removed', async () => {
|
|
149
|
+
mockFindFiles.mockResolvedValue(['setup.py']);
|
|
150
|
+
mockReadFile.mockResolvedValue(`
|
|
151
|
+
from distutils.core import setup
|
|
152
|
+
`);
|
|
153
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
154
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
155
|
+
expect(failures[0].details).toContain('distutils');
|
|
156
|
+
});
|
|
157
|
+
it('should flag typing.Dict as superseded', async () => {
|
|
158
|
+
mockFindFiles.mockResolvedValue(['models.py']);
|
|
159
|
+
mockReadFile.mockResolvedValue(`
|
|
160
|
+
from typing import Dict, List, Optional
|
|
161
|
+
`);
|
|
162
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
163
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
164
|
+
});
|
|
165
|
+
it('should not flag when disabled', async () => {
|
|
166
|
+
const disabled = new DeprecatedApisGate({ enabled: false });
|
|
167
|
+
const failures = await disabled.run({ cwd: '/project' });
|
|
168
|
+
expect(failures).toHaveLength(0);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('DeprecatedApisGate — Go', () => {
|
|
172
|
+
let gate;
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
gate = new DeprecatedApisGate();
|
|
175
|
+
vi.clearAllMocks();
|
|
176
|
+
});
|
|
177
|
+
it('should flag ioutil usage as deprecated', async () => {
|
|
178
|
+
mockFindFiles.mockResolvedValue(['main.go']);
|
|
179
|
+
mockReadFile.mockResolvedValue(`
|
|
180
|
+
package main
|
|
181
|
+
import "io/ioutil"
|
|
182
|
+
func main() {
|
|
183
|
+
data, _ := ioutil.ReadFile("test.txt")
|
|
184
|
+
ioutil.WriteFile("out.txt", data, 0644)
|
|
185
|
+
}
|
|
186
|
+
`);
|
|
187
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
188
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
189
|
+
expect(failures[0].details).toContain('ioutil');
|
|
190
|
+
});
|
|
191
|
+
it('should flag strings.Title as deprecated', async () => {
|
|
192
|
+
mockFindFiles.mockResolvedValue(['util.go']);
|
|
193
|
+
mockReadFile.mockResolvedValue(`
|
|
194
|
+
package util
|
|
195
|
+
import "strings"
|
|
196
|
+
func Title(s string) string {
|
|
197
|
+
return strings.Title(s)
|
|
198
|
+
}
|
|
199
|
+
`);
|
|
200
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
201
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
202
|
+
expect(failures[0].details).toContain('strings.Title');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('DeprecatedApisGate — C#', () => {
|
|
206
|
+
let gate;
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
gate = new DeprecatedApisGate();
|
|
209
|
+
vi.clearAllMocks();
|
|
210
|
+
});
|
|
211
|
+
it('should flag BinaryFormatter as security-deprecated', async () => {
|
|
212
|
+
mockFindFiles.mockResolvedValue(['Serializer.cs']);
|
|
213
|
+
mockReadFile.mockResolvedValue(`
|
|
214
|
+
using System.Runtime.Serialization.Formatters.Binary;
|
|
215
|
+
var formatter = new BinaryFormatter();
|
|
216
|
+
`);
|
|
217
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
218
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
219
|
+
expect(failures[0].details).toContain('BinaryFormatter');
|
|
220
|
+
});
|
|
221
|
+
it('should flag WebClient as deprecated', async () => {
|
|
222
|
+
mockFindFiles.mockResolvedValue(['HttpHelper.cs']);
|
|
223
|
+
mockReadFile.mockResolvedValue(`
|
|
224
|
+
using System.Net;
|
|
225
|
+
var client = new WebClient();
|
|
226
|
+
`);
|
|
227
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
228
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
229
|
+
expect(failures[0].details).toContain('WebClient');
|
|
230
|
+
});
|
|
231
|
+
it('should flag Thread.Abort as removed', async () => {
|
|
232
|
+
mockFindFiles.mockResolvedValue(['Worker.cs']);
|
|
233
|
+
mockReadFile.mockResolvedValue(`
|
|
234
|
+
using System.Threading;
|
|
235
|
+
Thread.Abort();
|
|
236
|
+
`);
|
|
237
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
238
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('DeprecatedApisGate — Java', () => {
|
|
242
|
+
let gate;
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
gate = new DeprecatedApisGate();
|
|
245
|
+
vi.clearAllMocks();
|
|
246
|
+
});
|
|
247
|
+
it('should flag Vector and Hashtable as deprecated', async () => {
|
|
248
|
+
mockFindFiles.mockResolvedValue(['Legacy.java']);
|
|
249
|
+
mockReadFile.mockResolvedValue(`
|
|
250
|
+
import java.util.*;
|
|
251
|
+
Vector<String> v = new Vector<String>();
|
|
252
|
+
Hashtable<String, Integer> ht = new Hashtable<String, Integer>();
|
|
253
|
+
`);
|
|
254
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
255
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
256
|
+
});
|
|
257
|
+
it('should flag new Integer() as deprecated', async () => {
|
|
258
|
+
mockFindFiles.mockResolvedValue(['Boxing.java']);
|
|
259
|
+
mockReadFile.mockResolvedValue(`
|
|
260
|
+
Integer x = new Integer(42);
|
|
261
|
+
Long y = new Long(100L);
|
|
262
|
+
`);
|
|
263
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
264
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
265
|
+
expect(failures[0].details).toMatch(/Integer|Long/);
|
|
266
|
+
});
|
|
267
|
+
it('should flag Thread.stop as security-deprecated', async () => {
|
|
268
|
+
mockFindFiles.mockResolvedValue(['ThreadManager.java']);
|
|
269
|
+
mockReadFile.mockResolvedValue(`
|
|
270
|
+
thread.stop();
|
|
271
|
+
`);
|
|
272
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
273
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
274
|
+
});
|
|
275
|
+
it('should flag finalize() as deprecated', async () => {
|
|
276
|
+
mockFindFiles.mockResolvedValue(['Resource.java']);
|
|
277
|
+
mockReadFile.mockResolvedValue(`
|
|
278
|
+
class Resource {
|
|
279
|
+
protected void finalize() throws Throwable {
|
|
280
|
+
super.finalize();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
`);
|
|
284
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
285
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
286
|
+
expect(failures[0].details).toContain('finalize');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phantom APIs Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects calls to non-existent methods/properties on known stdlib modules.
|
|
5
|
+
* AI models confidently generate method names that look correct but don't exist —
|
|
6
|
+
* e.g. fs.readFileAsync(), path.combine(), crypto.generateHash().
|
|
7
|
+
*
|
|
8
|
+
* This is the #2 most dangerous AI hallucination after package hallucination.
|
|
9
|
+
* Unlike type checkers, this gate catches phantom APIs even in plain JS, Python,
|
|
10
|
+
* and other dynamically-typed languages where the call would silently fail at runtime.
|
|
11
|
+
*
|
|
12
|
+
* Supported languages:
|
|
13
|
+
* JS/TS — Node.js 22.x builtins (fs, path, crypto, http, os, child_process, etc.)
|
|
14
|
+
* Python — stdlib modules (os, json, sys, re, datetime, pathlib, subprocess, etc.)
|
|
15
|
+
* Go — Common hallucinated stdlib patterns (strings vs bytes, os vs io, etc.)
|
|
16
|
+
* C# — Common .NET hallucinated APIs (LINQ, File I/O, string methods)
|
|
17
|
+
* Java — Common hallucinated JDK APIs (Collections, String, Stream, Files)
|
|
18
|
+
*
|
|
19
|
+
* @since v3.0.0
|
|
20
|
+
* @since v3.0.3 — Go, C#, Java pattern-based detection added
|
|
21
|
+
*/
|
|
22
|
+
import { Gate, GateContext } from './base.js';
|
|
23
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
24
|
+
export interface PhantomApiCall {
|
|
25
|
+
file: string;
|
|
26
|
+
line: number;
|
|
27
|
+
module: string;
|
|
28
|
+
method: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
export interface PhantomApisConfig {
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
check_node?: boolean;
|
|
34
|
+
check_python?: boolean;
|
|
35
|
+
check_go?: boolean;
|
|
36
|
+
check_csharp?: boolean;
|
|
37
|
+
check_java?: boolean;
|
|
38
|
+
ignore_patterns?: string[];
|
|
39
|
+
}
|
|
40
|
+
export declare class PhantomApisGate extends Gate {
|
|
41
|
+
private config;
|
|
42
|
+
constructor(config?: PhantomApisConfig);
|
|
43
|
+
protected get provenance(): Provenance;
|
|
44
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Node.js stdlib method verification.
|
|
47
|
+
* For each known module, we maintain the actual exported methods.
|
|
48
|
+
* Any call like fs.readFileAsync() that doesn't match is flagged.
|
|
49
|
+
*/
|
|
50
|
+
private checkNodePhantomApis;
|
|
51
|
+
/**
|
|
52
|
+
* Python stdlib method verification.
|
|
53
|
+
*/
|
|
54
|
+
private checkPythonPhantomApis;
|
|
55
|
+
/** Suggest the closest real method name (Levenshtein distance ≤ 3) */
|
|
56
|
+
private suggestNodeMethod;
|
|
57
|
+
private suggestPythonMethod;
|
|
58
|
+
private findClosest;
|
|
59
|
+
private levenshtein;
|
|
60
|
+
/**
|
|
61
|
+
* Go phantom API detection — pattern-based.
|
|
62
|
+
* AI commonly hallucinates Python/JS-style method names on Go packages.
|
|
63
|
+
* e.g. strings.Contains() exists, but strings.includes() doesn't.
|
|
64
|
+
*/
|
|
65
|
+
private checkGoPhantomApis;
|
|
66
|
+
/**
|
|
67
|
+
* C# phantom API detection — pattern-based.
|
|
68
|
+
* AI hallucinates Java/Python-style method names on .NET types.
|
|
69
|
+
*/
|
|
70
|
+
private checkCSharpPhantomApis;
|
|
71
|
+
/**
|
|
72
|
+
* Java/Kotlin phantom API detection — pattern-based.
|
|
73
|
+
* AI hallucinates Python/JS-style APIs on JDK classes.
|
|
74
|
+
*/
|
|
75
|
+
private checkJavaPhantomApis;
|
|
76
|
+
private escapeRegex;
|
|
77
|
+
}
|