@continum/cli 0.1.0
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/README.md +481 -0
- package/SETUP.md +517 -0
- package/dist/api/client.d.ts +17 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +70 -0
- package/dist/api/client.js.map +1 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +104 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +217 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/patterns.d.ts +3 -0
- package/dist/commands/patterns.d.ts.map +1 -0
- package/dist/commands/patterns.js +67 -0
- package/dist/commands/patterns.js.map +1 -0
- package/dist/commands/scan.d.ts +11 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +219 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +87 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/config/default-config.d.ts +3 -0
- package/dist/config/default-config.d.ts.map +1 -0
- package/dist/config/default-config.js +25 -0
- package/dist/config/default-config.js.map +1 -0
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +96 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/git/git-utils.d.ts +8 -0
- package/dist/git/git-utils.d.ts.map +1 -0
- package/dist/git/git-utils.js +130 -0
- package/dist/git/git-utils.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner/local-scan.d.ts +15 -0
- package/dist/scanner/local-scan.d.ts.map +1 -0
- package/dist/scanner/local-scan.js +227 -0
- package/dist/scanner/local-scan.js.map +1 -0
- package/dist/scanner/pattern-updater.d.ts +12 -0
- package/dist/scanner/pattern-updater.d.ts.map +1 -0
- package/dist/scanner/pattern-updater.js +110 -0
- package/dist/scanner/pattern-updater.js.map +1 -0
- package/dist/scanner/patterns.d.ts +5 -0
- package/dist/scanner/patterns.d.ts.map +1 -0
- package/dist/scanner/patterns.js +145 -0
- package/dist/scanner/patterns.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
- package/src/api/client.ts +77 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/login.ts +205 -0
- package/src/commands/patterns.ts +68 -0
- package/src/commands/scan.ts +257 -0
- package/src/commands/status.ts +57 -0
- package/src/commands/uninstall.ts +55 -0
- package/src/config/default-config.ts +23 -0
- package/src/config/loader.ts +67 -0
- package/src/git/git-utils.ts +95 -0
- package/src/index.ts +72 -0
- package/src/scanner/local-scan.ts +222 -0
- package/src/scanner/pattern-updater.ts +94 -0
- package/src/scanner/patterns.ts +156 -0
- package/src/types.ts +64 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { ScanResult, Violation, UnknownPattern, Pattern, ContinumConfig } from '../types';
|
|
4
|
+
import { BUILTIN_PATTERNS, isHighEntropyCredential, calculateEntropy } from './patterns';
|
|
5
|
+
|
|
6
|
+
export class LocalScanner {
|
|
7
|
+
private patterns: Pattern[] = [];
|
|
8
|
+
private config: ContinumConfig;
|
|
9
|
+
|
|
10
|
+
constructor(config: ContinumConfig, customPatterns: Pattern[] = []) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.patterns = [...BUILTIN_PATTERNS, ...customPatterns];
|
|
13
|
+
|
|
14
|
+
// Add custom regex patterns from config
|
|
15
|
+
if (config.patterns?.custom) {
|
|
16
|
+
config.patterns.custom.forEach((pattern, index) => {
|
|
17
|
+
this.patterns.push({
|
|
18
|
+
pattern,
|
|
19
|
+
patternType: 'CUSTOM',
|
|
20
|
+
description: `Custom Pattern ${index + 1}`,
|
|
21
|
+
severity: 'HIGH'
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async scanFiles(files: string[]): Promise<ScanResult> {
|
|
28
|
+
const violations: Violation[] = [];
|
|
29
|
+
const unknownPatterns: UnknownPattern[] = [];
|
|
30
|
+
let filesScanned = 0;
|
|
31
|
+
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
// Skip ignored files
|
|
34
|
+
if (this.shouldIgnore(file)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
40
|
+
const lines = content.split('\n');
|
|
41
|
+
|
|
42
|
+
filesScanned++;
|
|
43
|
+
|
|
44
|
+
// Scan with known patterns
|
|
45
|
+
const fileViolations = this.scanContent(content, file, lines);
|
|
46
|
+
violations.push(...fileViolations);
|
|
47
|
+
|
|
48
|
+
// Detect high-entropy strings that might be credentials
|
|
49
|
+
const possibleCredentials = this.detectHighEntropyStrings(content, file, lines);
|
|
50
|
+
unknownPatterns.push(...possibleCredentials);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Skip files that can't be read (binary, etc.)
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
violations,
|
|
59
|
+
unknownPatterns,
|
|
60
|
+
filesScanned,
|
|
61
|
+
clean: violations.length === 0 && unknownPatterns.length === 0
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private scanContent(content: string, file: string, lines: string[]): Violation[] {
|
|
66
|
+
const violations: Violation[] = [];
|
|
67
|
+
|
|
68
|
+
for (const pattern of this.patterns) {
|
|
69
|
+
const regex = new RegExp(pattern.pattern, 'gi');
|
|
70
|
+
let match;
|
|
71
|
+
|
|
72
|
+
while ((match = regex.exec(content)) !== null) {
|
|
73
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
74
|
+
|
|
75
|
+
violations.push({
|
|
76
|
+
file,
|
|
77
|
+
line: lineNumber,
|
|
78
|
+
type: pattern.patternType,
|
|
79
|
+
pattern: pattern.pattern,
|
|
80
|
+
value: match[0],
|
|
81
|
+
severity: pattern.severity,
|
|
82
|
+
message: pattern.description
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return violations;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private detectHighEntropyStrings(content: string, file: string, lines: string[]): UnknownPattern[] {
|
|
91
|
+
const unknownPatterns: UnknownPattern[] = [];
|
|
92
|
+
|
|
93
|
+
// Look for string assignments with high entropy
|
|
94
|
+
const stringPatterns = [
|
|
95
|
+
/(\w+)\s*[:=]\s*["']([a-zA-Z0-9_\-+=\/]{16,})["']/g,
|
|
96
|
+
/const\s+(\w+)\s*=\s*["']([a-zA-Z0-9_\-+=\/]{16,})["']/g,
|
|
97
|
+
/let\s+(\w+)\s*=\s*["']([a-zA-Z0-9_\-+=\/]{16,})["']/g,
|
|
98
|
+
/var\s+(\w+)\s*=\s*["']([a-zA-Z0-9_\-+=\/]{16,})["']/g
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
for (const pattern of stringPatterns) {
|
|
102
|
+
let match;
|
|
103
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
104
|
+
const variableName = match[1];
|
|
105
|
+
const value = match[2];
|
|
106
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
107
|
+
|
|
108
|
+
// Get surrounding context
|
|
109
|
+
const contextStart = Math.max(0, lineNumber - 3);
|
|
110
|
+
const contextEnd = Math.min(lines.length, lineNumber + 2);
|
|
111
|
+
const surroundingCode = lines.slice(contextStart, contextEnd).join('\n');
|
|
112
|
+
|
|
113
|
+
// Check if this looks like a credential
|
|
114
|
+
if (isHighEntropyCredential(value, variableName + ' ' + surroundingCode)) {
|
|
115
|
+
// Check if it matches any known pattern
|
|
116
|
+
const matchesKnown = this.patterns.some(p =>
|
|
117
|
+
new RegExp(p.pattern, 'i').test(value)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (!matchesKnown) {
|
|
121
|
+
const suggestedPattern = this.extractPattern(value);
|
|
122
|
+
const confidence = this.calculateConfidence(value, variableName);
|
|
123
|
+
|
|
124
|
+
unknownPatterns.push({
|
|
125
|
+
value,
|
|
126
|
+
suggestedPattern,
|
|
127
|
+
confidence,
|
|
128
|
+
context: {
|
|
129
|
+
file,
|
|
130
|
+
line: lineNumber,
|
|
131
|
+
variableName,
|
|
132
|
+
surroundingCode
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return unknownPatterns;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private extractPattern(value: string): string {
|
|
144
|
+
// Try to extract a pattern from the value
|
|
145
|
+
const hasPrefix = /^[a-z]{2,}_/.test(value);
|
|
146
|
+
|
|
147
|
+
if (hasPrefix) {
|
|
148
|
+
const prefix = value.match(/^[a-z_]+/)?.[0] || '';
|
|
149
|
+
const rest = value.slice(prefix.length);
|
|
150
|
+
|
|
151
|
+
// Determine character set
|
|
152
|
+
const hasUppercase = /[A-Z]/.test(rest);
|
|
153
|
+
const hasLowercase = /[a-z]/.test(rest);
|
|
154
|
+
const hasDigits = /[0-9]/.test(rest);
|
|
155
|
+
const hasSpecial = /[_\-]/.test(rest);
|
|
156
|
+
|
|
157
|
+
let charSet = '';
|
|
158
|
+
if (hasUppercase) charSet += 'A-Z';
|
|
159
|
+
if (hasLowercase) charSet += 'a-z';
|
|
160
|
+
if (hasDigits) charSet += '0-9';
|
|
161
|
+
if (hasSpecial) charSet += '_\\-';
|
|
162
|
+
|
|
163
|
+
return `${prefix}[${charSet}]{${rest.length}}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// No clear prefix, just match character set and length
|
|
167
|
+
const hasUppercase = /[A-Z]/.test(value);
|
|
168
|
+
const hasLowercase = /[a-z]/.test(value);
|
|
169
|
+
const hasDigits = /[0-9]/.test(value);
|
|
170
|
+
const hasSpecial = /[_\-+=\/]/.test(value);
|
|
171
|
+
|
|
172
|
+
let charSet = '';
|
|
173
|
+
if (hasUppercase) charSet += 'A-Z';
|
|
174
|
+
if (hasLowercase) charSet += 'a-z';
|
|
175
|
+
if (hasDigits) charSet += '0-9';
|
|
176
|
+
if (hasSpecial) charSet += '_\\-+=\\/';
|
|
177
|
+
|
|
178
|
+
return `[${charSet}]{${value.length}}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private calculateConfidence(value: string, variableName: string): 'HIGH' | 'MEDIUM' | 'LOW' {
|
|
182
|
+
const entropy = calculateEntropy(value);
|
|
183
|
+
const suspiciousNames = ['key', 'token', 'secret', 'password', 'credential', 'auth', 'api'];
|
|
184
|
+
const hasSuspiciousName = suspiciousNames.some(name =>
|
|
185
|
+
variableName.toLowerCase().includes(name)
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (entropy > 5.0 && hasSuspiciousName) return 'HIGH';
|
|
189
|
+
if (entropy > 4.5 && hasSuspiciousName) return 'MEDIUM';
|
|
190
|
+
if (entropy > 5.5) return 'MEDIUM';
|
|
191
|
+
return 'LOW';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private getLineNumber(content: string, index: number): number {
|
|
195
|
+
return content.substring(0, index).split('\n').length;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private shouldIgnore(file: string): boolean {
|
|
199
|
+
const normalizedFile = file.replace(/\\/g, '/');
|
|
200
|
+
|
|
201
|
+
return this.config.ignore.some(pattern => {
|
|
202
|
+
// Convert glob pattern to regex
|
|
203
|
+
const regexPattern = pattern
|
|
204
|
+
.replace(/\*\*/g, '.*')
|
|
205
|
+
.replace(/\*/g, '[^/]*')
|
|
206
|
+
.replace(/\?/g, '.');
|
|
207
|
+
|
|
208
|
+
const regex = new RegExp(regexPattern);
|
|
209
|
+
return regex.test(normalizedFile);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
redactValue(value: string): string {
|
|
214
|
+
if (value.length <= 8) {
|
|
215
|
+
return '•'.repeat(value.length);
|
|
216
|
+
}
|
|
217
|
+
const visibleChars = 4;
|
|
218
|
+
const start = value.substring(0, visibleChars);
|
|
219
|
+
const redacted = '•'.repeat(Math.min(10, value.length - visibleChars));
|
|
220
|
+
return start + redacted;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { ContinumApiClient } from '../api/client';
|
|
3
|
+
import { Pattern } from '../types';
|
|
4
|
+
import { getCacheFile, getCacheDir } from '../config/loader';
|
|
5
|
+
import { BUILTIN_PATTERNS } from './patterns';
|
|
6
|
+
|
|
7
|
+
interface PatternCache {
|
|
8
|
+
builtin: Pattern[];
|
|
9
|
+
customer: Pattern[];
|
|
10
|
+
global: Pattern[];
|
|
11
|
+
lastUpdate: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class PatternUpdater {
|
|
15
|
+
private client: ContinumApiClient;
|
|
16
|
+
private cacheFile: string;
|
|
17
|
+
|
|
18
|
+
constructor(client: ContinumApiClient) {
|
|
19
|
+
this.client = client;
|
|
20
|
+
this.cacheFile = getCacheFile();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async updatePatterns(force: boolean = false): Promise<void> {
|
|
24
|
+
const cache = this.loadCache();
|
|
25
|
+
|
|
26
|
+
// Update every 24 hours, or on force
|
|
27
|
+
if (!force && Date.now() - cache.lastUpdate < 86400000) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Fetch patterns from API
|
|
33
|
+
const patterns = await this.client.getPatternLibrary();
|
|
34
|
+
|
|
35
|
+
// Separate customer and global patterns
|
|
36
|
+
const customerPatterns = patterns.filter(p => !('isGlobal' in p) || !(p as any).isGlobal);
|
|
37
|
+
const globalPatterns = patterns.filter(p => (p as any).isGlobal);
|
|
38
|
+
|
|
39
|
+
// Update cache
|
|
40
|
+
const newCache: PatternCache = {
|
|
41
|
+
builtin: BUILTIN_PATTERNS,
|
|
42
|
+
customer: customerPatterns,
|
|
43
|
+
global: globalPatterns,
|
|
44
|
+
lastUpdate: Date.now()
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.saveCache(newCache);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// If update fails, continue with cached patterns
|
|
50
|
+
console.warn('Failed to update patterns, using cache');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
loadPatterns(): Pattern[] {
|
|
55
|
+
const cache = this.loadCache();
|
|
56
|
+
return [
|
|
57
|
+
...cache.builtin,
|
|
58
|
+
...cache.customer,
|
|
59
|
+
...cache.global
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private loadCache(): PatternCache {
|
|
64
|
+
if (!fs.existsSync(this.cacheFile)) {
|
|
65
|
+
return {
|
|
66
|
+
builtin: BUILTIN_PATTERNS,
|
|
67
|
+
customer: [],
|
|
68
|
+
global: [],
|
|
69
|
+
lastUpdate: 0
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const content = fs.readFileSync(this.cacheFile, 'utf-8');
|
|
75
|
+
return JSON.parse(content);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
builtin: BUILTIN_PATTERNS,
|
|
79
|
+
customer: [],
|
|
80
|
+
global: [],
|
|
81
|
+
lastUpdate: 0
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private saveCache(cache: PatternCache): void {
|
|
87
|
+
const cacheDir = getCacheDir();
|
|
88
|
+
if (!fs.existsSync(cacheDir)) {
|
|
89
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(this.cacheFile, JSON.stringify(cache, null, 2));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Pattern } from '../types';
|
|
2
|
+
|
|
3
|
+
// Built-in credential patterns
|
|
4
|
+
export const BUILTIN_PATTERNS: Pattern[] = [
|
|
5
|
+
// AWS
|
|
6
|
+
{
|
|
7
|
+
pattern: 'AKIA[0-9A-Z]{16}',
|
|
8
|
+
patternType: 'AWS_ACCESS_KEY',
|
|
9
|
+
description: 'AWS Access Key ID',
|
|
10
|
+
severity: 'CRITICAL'
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: 'aws_secret_access_key\\s*=\\s*["\']?([A-Za-z0-9/+=]{40})["\']?',
|
|
14
|
+
patternType: 'AWS_SECRET_KEY',
|
|
15
|
+
description: 'AWS Secret Access Key',
|
|
16
|
+
severity: 'CRITICAL'
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// Stripe
|
|
20
|
+
{
|
|
21
|
+
pattern: 'sk_live_[0-9a-zA-Z]{24,}',
|
|
22
|
+
patternType: 'STRIPE_LIVE_KEY',
|
|
23
|
+
description: 'Stripe Live Secret Key',
|
|
24
|
+
severity: 'CRITICAL'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: 'rk_live_[0-9a-zA-Z]{24,}',
|
|
28
|
+
patternType: 'STRIPE_RESTRICTED_KEY',
|
|
29
|
+
description: 'Stripe Restricted Key',
|
|
30
|
+
severity: 'HIGH'
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// GitHub
|
|
34
|
+
{
|
|
35
|
+
pattern: 'ghp_[0-9a-zA-Z]{36}',
|
|
36
|
+
patternType: 'GITHUB_PAT',
|
|
37
|
+
description: 'GitHub Personal Access Token',
|
|
38
|
+
severity: 'CRITICAL'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
pattern: 'gho_[0-9a-zA-Z]{36}',
|
|
42
|
+
patternType: 'GITHUB_OAUTH',
|
|
43
|
+
description: 'GitHub OAuth Token',
|
|
44
|
+
severity: 'CRITICAL'
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Anthropic
|
|
48
|
+
{
|
|
49
|
+
pattern: 'sk-ant-api03-[0-9a-zA-Z_-]{95}',
|
|
50
|
+
patternType: 'ANTHROPIC_API_KEY',
|
|
51
|
+
description: 'Anthropic API Key',
|
|
52
|
+
severity: 'CRITICAL'
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// OpenAI
|
|
56
|
+
{
|
|
57
|
+
pattern: 'sk-[a-zA-Z0-9]{48}',
|
|
58
|
+
patternType: 'OPENAI_API_KEY',
|
|
59
|
+
description: 'OpenAI API Key',
|
|
60
|
+
severity: 'CRITICAL'
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Database Connection Strings
|
|
64
|
+
{
|
|
65
|
+
pattern: 'postgresql://[^\\s:]+:[^\\s@]+@[^\\s/]+',
|
|
66
|
+
patternType: 'POSTGRES_CONNECTION',
|
|
67
|
+
description: 'PostgreSQL Connection String with Password',
|
|
68
|
+
severity: 'CRITICAL'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
pattern: 'mysql://[^\\s:]+:[^\\s@]+@[^\\s/]+',
|
|
72
|
+
patternType: 'MYSQL_CONNECTION',
|
|
73
|
+
description: 'MySQL Connection String with Password',
|
|
74
|
+
severity: 'CRITICAL'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
pattern: 'mongodb(\\+srv)?://[^\\s:]+:[^\\s@]+@[^\\s/]+',
|
|
78
|
+
patternType: 'MONGODB_CONNECTION',
|
|
79
|
+
description: 'MongoDB Connection String with Password',
|
|
80
|
+
severity: 'CRITICAL'
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Private Keys
|
|
84
|
+
{
|
|
85
|
+
pattern: '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----',
|
|
86
|
+
patternType: 'PRIVATE_KEY',
|
|
87
|
+
description: 'Private Key (PEM format)',
|
|
88
|
+
severity: 'CRITICAL'
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// Generic API Keys (high entropy)
|
|
92
|
+
{
|
|
93
|
+
pattern: '(api[_-]?key|apikey|api[_-]?secret)\\s*[:=]\\s*["\']([a-zA-Z0-9_\\-]{32,})["\']',
|
|
94
|
+
patternType: 'GENERIC_API_KEY',
|
|
95
|
+
description: 'Generic API Key',
|
|
96
|
+
severity: 'HIGH'
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// JWT Tokens
|
|
100
|
+
{
|
|
101
|
+
pattern: 'eyJ[a-zA-Z0-9_-]*\\.eyJ[a-zA-Z0-9_-]*\\.[a-zA-Z0-9_-]*',
|
|
102
|
+
patternType: 'JWT_TOKEN',
|
|
103
|
+
description: 'JWT Token',
|
|
104
|
+
severity: 'HIGH'
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// UK PII
|
|
108
|
+
{
|
|
109
|
+
pattern: '\\b[A-Z]{2}[0-9]{6}[A-Z]?\\b',
|
|
110
|
+
patternType: 'NHS_NUMBER',
|
|
111
|
+
description: 'NHS Number',
|
|
112
|
+
severity: 'HIGH'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
pattern: '\\b[A-Z]{2}\\s?[0-9]{2}\\s?[0-9]{2}\\s?[0-9]{2}\\s?[A-Z]\\b',
|
|
116
|
+
patternType: 'NI_NUMBER',
|
|
117
|
+
description: 'National Insurance Number',
|
|
118
|
+
severity: 'HIGH'
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Calculate entropy of a string (measure of randomness)
|
|
123
|
+
export function calculateEntropy(str: string): number {
|
|
124
|
+
const len = str.length;
|
|
125
|
+
const frequencies: { [key: string]: number } = {};
|
|
126
|
+
|
|
127
|
+
for (const char of str) {
|
|
128
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let entropy = 0;
|
|
132
|
+
for (const char in frequencies) {
|
|
133
|
+
const p = frequencies[char] / len;
|
|
134
|
+
entropy -= p * Math.log2(p);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return entropy;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if a string looks like a credential based on entropy and context
|
|
141
|
+
export function isHighEntropyCredential(value: string, context: string): boolean {
|
|
142
|
+
// Must be long enough
|
|
143
|
+
if (value.length < 16) return false;
|
|
144
|
+
|
|
145
|
+
// Must have high entropy (randomness)
|
|
146
|
+
const entropy = calculateEntropy(value);
|
|
147
|
+
if (entropy < 4.5) return false;
|
|
148
|
+
|
|
149
|
+
// Must be in a suspicious context
|
|
150
|
+
const suspiciousPatterns = [
|
|
151
|
+
/\b(key|token|secret|password|credential|auth|api)\b/i,
|
|
152
|
+
/\b(access|private|bearer|jwt)\b/i
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
return suspiciousPatterns.some(pattern => pattern.test(context));
|
|
156
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface ContinumConfig {
|
|
2
|
+
scanOnCommit: boolean;
|
|
3
|
+
sandbox: string;
|
|
4
|
+
apiUrl?: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
block: RiskLevel[];
|
|
7
|
+
warn: RiskLevel[];
|
|
8
|
+
ignore: string[];
|
|
9
|
+
patterns?: {
|
|
10
|
+
custom?: string[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type RiskLevel = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
|
15
|
+
|
|
16
|
+
export interface Violation {
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
type: string;
|
|
20
|
+
pattern: string;
|
|
21
|
+
value: string;
|
|
22
|
+
severity: RiskLevel;
|
|
23
|
+
message?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UnknownPattern {
|
|
27
|
+
value: string;
|
|
28
|
+
suggestedPattern: string;
|
|
29
|
+
confidence: 'HIGH' | 'MEDIUM' | 'LOW';
|
|
30
|
+
context: {
|
|
31
|
+
file: string;
|
|
32
|
+
line: number;
|
|
33
|
+
variableName?: string;
|
|
34
|
+
surroundingCode?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScanResult {
|
|
39
|
+
violations: Violation[];
|
|
40
|
+
unknownPatterns: UnknownPattern[];
|
|
41
|
+
filesScanned: number;
|
|
42
|
+
clean: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Pattern {
|
|
46
|
+
pattern: string;
|
|
47
|
+
patternType: string;
|
|
48
|
+
description: string;
|
|
49
|
+
severity: RiskLevel;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ApprovePatternDto {
|
|
53
|
+
pattern: string;
|
|
54
|
+
patternType: string;
|
|
55
|
+
description: string;
|
|
56
|
+
severity: RiskLevel;
|
|
57
|
+
exampleValue: string;
|
|
58
|
+
confidence: string;
|
|
59
|
+
context: {
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
variableName?: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|