@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,257 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { LocalScanner } from '../scanner/local-scan';
|
|
6
|
+
import { loadConfig, loadCredentials } from '../config/loader';
|
|
7
|
+
import { getStagedFiles, getStagedDiff } from '../git/git-utils';
|
|
8
|
+
import { ContinumApiClient } from '../api/client';
|
|
9
|
+
import { PatternUpdater } from '../scanner/pattern-updater';
|
|
10
|
+
import { UnknownPattern, RiskLevel } from '../types';
|
|
11
|
+
|
|
12
|
+
interface ScanOptions {
|
|
13
|
+
staged?: boolean;
|
|
14
|
+
hook?: boolean;
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
autoApprove?: boolean;
|
|
17
|
+
warnOnly?: boolean;
|
|
18
|
+
files?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function scanCommand(options: ScanOptions): Promise<void> {
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
const credentials = loadCredentials();
|
|
24
|
+
|
|
25
|
+
// Update patterns if we have credentials
|
|
26
|
+
let customPatterns: any[] = [];
|
|
27
|
+
if (credentials.apiUrl && credentials.apiKey) {
|
|
28
|
+
try {
|
|
29
|
+
const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
|
|
30
|
+
const updater = new PatternUpdater(client);
|
|
31
|
+
await updater.updatePatterns();
|
|
32
|
+
customPatterns = updater.loadPatterns();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Continue with built-in patterns only
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Determine which files to scan
|
|
39
|
+
let filesToScan: string[] = [];
|
|
40
|
+
|
|
41
|
+
if (options.staged) {
|
|
42
|
+
filesToScan = getStagedFiles();
|
|
43
|
+
} else if (options.files && options.files.length > 0) {
|
|
44
|
+
filesToScan = options.files;
|
|
45
|
+
} else {
|
|
46
|
+
console.log(chalk.red('✗ No files specified'));
|
|
47
|
+
console.log(chalk.gray(' Use --staged to scan staged files, or provide file paths'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (filesToScan.length === 0) {
|
|
52
|
+
console.log(chalk.yellow('⚠️ No files to scan'));
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Run scan
|
|
57
|
+
if (!options.hook) {
|
|
58
|
+
console.log(chalk.blue(`\nContinum — scanning ${filesToScan.length} file(s)...\n`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const scanner = new LocalScanner(config, customPatterns);
|
|
62
|
+
const result = await scanner.scanFiles(filesToScan);
|
|
63
|
+
|
|
64
|
+
// Handle violations
|
|
65
|
+
if (result.violations.length > 0) {
|
|
66
|
+
console.log(chalk.red.bold('❌ BLOCKED\n'));
|
|
67
|
+
|
|
68
|
+
for (const violation of result.violations) {
|
|
69
|
+
const shouldBlock = config.block.includes(violation.severity);
|
|
70
|
+
const shouldWarn = config.warn.includes(violation.severity);
|
|
71
|
+
|
|
72
|
+
if (shouldBlock || shouldWarn) {
|
|
73
|
+
console.log(chalk.gray(`${violation.file} (line ${violation.line})`));
|
|
74
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
75
|
+
console.log(`Type: ${violation.type}`);
|
|
76
|
+
console.log(`Found: ${scanner.redactValue(violation.value)}`);
|
|
77
|
+
console.log(`Severity: ${getSeverityColor(violation.severity)(violation.severity)}`);
|
|
78
|
+
if (violation.message) {
|
|
79
|
+
console.log(`Message: ${violation.message}`);
|
|
80
|
+
}
|
|
81
|
+
console.log();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const blockedCount = result.violations.filter(v =>
|
|
86
|
+
config.block.includes(v.severity)
|
|
87
|
+
).length;
|
|
88
|
+
|
|
89
|
+
if (blockedCount > 0 && !options.warnOnly) {
|
|
90
|
+
console.log(chalk.red('Fix these before committing.'));
|
|
91
|
+
console.log(chalk.gray('Override (not recommended): git commit --no-verify\n'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle unknown patterns
|
|
97
|
+
if (result.unknownPatterns.length > 0 && !options.autoApprove) {
|
|
98
|
+
for (const pattern of result.unknownPatterns) {
|
|
99
|
+
await handleUnknownPattern(pattern, scanner, credentials, options);
|
|
100
|
+
}
|
|
101
|
+
} else if (result.unknownPatterns.length > 0 && options.autoApprove) {
|
|
102
|
+
// Auto-approve all patterns
|
|
103
|
+
for (const pattern of result.unknownPatterns) {
|
|
104
|
+
await approvePattern(pattern, scanner, credentials, 'HIGH');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Send background audit if we have credentials
|
|
109
|
+
if (credentials.apiUrl && credentials.apiKey && options.staged) {
|
|
110
|
+
try {
|
|
111
|
+
const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
|
|
112
|
+
const diff = getStagedDiff();
|
|
113
|
+
await client.sendSandboxAudit(diff, config.sandbox);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Silent fail for background audit
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Success
|
|
120
|
+
if (result.clean) {
|
|
121
|
+
if (!options.hook) {
|
|
122
|
+
console.log(chalk.green('✓ Clean\n'));
|
|
123
|
+
}
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function handleUnknownPattern(
|
|
129
|
+
pattern: UnknownPattern,
|
|
130
|
+
scanner: LocalScanner,
|
|
131
|
+
credentials: { apiUrl?: string; apiKey?: string },
|
|
132
|
+
options: ScanOptions
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
console.log(chalk.yellow('\n⚠️ POSSIBLE CREDENTIAL DETECTED\n'));
|
|
135
|
+
console.log(chalk.gray(`${pattern.context.file} (line ${pattern.context.line})`));
|
|
136
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
137
|
+
console.log(`Type: UNKNOWN_PATTERN (${pattern.confidence} confidence)`);
|
|
138
|
+
console.log(`Found: ${scanner.redactValue(pattern.value)}`);
|
|
139
|
+
console.log(`Pattern: ${pattern.suggestedPattern}`);
|
|
140
|
+
console.log('\nThis looks like a credential, but it\'s not in our pattern library.\n');
|
|
141
|
+
|
|
142
|
+
if (options.strict) {
|
|
143
|
+
console.log(chalk.red('Commit blocked (strict mode)'));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { action } = await prompts({
|
|
148
|
+
type: 'select',
|
|
149
|
+
name: 'action',
|
|
150
|
+
message: 'What would you like to do?',
|
|
151
|
+
choices: [
|
|
152
|
+
{ title: 'Block this commit', value: 'block' },
|
|
153
|
+
{ title: 'Approve pattern and block (will catch in future)', value: 'approve' },
|
|
154
|
+
{ title: 'Ignore this pattern', value: 'ignore' },
|
|
155
|
+
{ title: 'Continue anyway (not recommended)', value: 'continue' }
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
switch (action) {
|
|
160
|
+
case 'approve':
|
|
161
|
+
await approvePattern(pattern, scanner, credentials);
|
|
162
|
+
console.log(chalk.green('✓ Pattern saved to your library'));
|
|
163
|
+
console.log(chalk.green('✓ This pattern will now be caught locally on future commits\n'));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
|
|
166
|
+
case 'block':
|
|
167
|
+
process.exit(1);
|
|
168
|
+
|
|
169
|
+
case 'ignore':
|
|
170
|
+
console.log(chalk.gray('Pattern ignored\n'));
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case 'continue':
|
|
174
|
+
console.log(chalk.yellow('⚠️ Continuing without blocking\n'));
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function approvePattern(
|
|
180
|
+
pattern: UnknownPattern,
|
|
181
|
+
scanner: LocalScanner,
|
|
182
|
+
credentials: { apiUrl?: string; apiKey?: string },
|
|
183
|
+
defaultSeverity?: RiskLevel
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
if (!credentials.apiUrl || !credentials.apiKey) {
|
|
186
|
+
console.log(chalk.red('✗ No API credentials configured'));
|
|
187
|
+
console.log(chalk.gray(' Run `continum init` to set up credentials'));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let description = `Custom pattern for ${pattern.context.variableName || 'credential'}`;
|
|
192
|
+
let severity: RiskLevel = defaultSeverity || 'HIGH';
|
|
193
|
+
|
|
194
|
+
if (!defaultSeverity) {
|
|
195
|
+
const response = await prompts([
|
|
196
|
+
{
|
|
197
|
+
type: 'text',
|
|
198
|
+
name: 'description',
|
|
199
|
+
message: 'Pattern description:',
|
|
200
|
+
initial: description
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: 'select',
|
|
204
|
+
name: 'severity',
|
|
205
|
+
message: 'Severity level:',
|
|
206
|
+
choices: [
|
|
207
|
+
{ title: 'Critical', value: 'CRITICAL' },
|
|
208
|
+
{ title: 'High', value: 'HIGH' },
|
|
209
|
+
{ title: 'Medium', value: 'MEDIUM' }
|
|
210
|
+
],
|
|
211
|
+
initial: 1
|
|
212
|
+
}
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
if (response.description) description = response.description;
|
|
216
|
+
if (response.severity) severity = response.severity;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
|
|
221
|
+
await client.approvePattern({
|
|
222
|
+
pattern: pattern.suggestedPattern,
|
|
223
|
+
patternType: 'CUSTOM',
|
|
224
|
+
description,
|
|
225
|
+
severity,
|
|
226
|
+
exampleValue: scanner.redactValue(pattern.value),
|
|
227
|
+
confidence: pattern.confidence,
|
|
228
|
+
context: {
|
|
229
|
+
file: pattern.context.file,
|
|
230
|
+
line: pattern.context.line,
|
|
231
|
+
variableName: pattern.context.variableName
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Update local cache
|
|
236
|
+
const updater = new PatternUpdater(client);
|
|
237
|
+
await updater.updatePatterns(true);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.log(chalk.red('✗ Failed to save pattern'));
|
|
240
|
+
console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getSeverityColor(severity: RiskLevel) {
|
|
245
|
+
switch (severity) {
|
|
246
|
+
case 'CRITICAL':
|
|
247
|
+
return chalk.red.bold;
|
|
248
|
+
case 'HIGH':
|
|
249
|
+
return chalk.red;
|
|
250
|
+
case 'MEDIUM':
|
|
251
|
+
return chalk.yellow;
|
|
252
|
+
case 'LOW':
|
|
253
|
+
return chalk.gray;
|
|
254
|
+
default:
|
|
255
|
+
return chalk.white;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, loadCredentials } from '../config/loader';
|
|
3
|
+
import { ContinumApiClient } from '../api/client';
|
|
4
|
+
import { hasPreCommitHook, isGitRepository } from '../git/git-utils';
|
|
5
|
+
|
|
6
|
+
export async function statusCommand(): Promise<void> {
|
|
7
|
+
console.log(chalk.blue.bold('\n🛡️ Continum Status\n'));
|
|
8
|
+
|
|
9
|
+
// Check git repository
|
|
10
|
+
if (!isGitRepository()) {
|
|
11
|
+
console.log(chalk.red('✗ Not in a git repository'));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(chalk.green('✓ Git repository detected'));
|
|
15
|
+
|
|
16
|
+
// Check config file
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
if (config) {
|
|
19
|
+
console.log(chalk.green('✓ Configuration file found'));
|
|
20
|
+
console.log(chalk.gray(` Sandbox: ${config.sandbox}`));
|
|
21
|
+
console.log(chalk.gray(` Block: ${config.block.join(', ')}`));
|
|
22
|
+
console.log(chalk.gray(` Warn: ${config.warn.join(', ')}`));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(chalk.yellow('⚠️ No configuration file'));
|
|
25
|
+
console.log(chalk.gray(' Run `continum init` to create one'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check pre-commit hook
|
|
29
|
+
if (hasPreCommitHook()) {
|
|
30
|
+
console.log(chalk.green('✓ Pre-commit hook installed'));
|
|
31
|
+
} else {
|
|
32
|
+
console.log(chalk.yellow('⚠️ Pre-commit hook not installed'));
|
|
33
|
+
console.log(chalk.gray(' Run `continum init` to install'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check API connection
|
|
37
|
+
const credentials = loadCredentials();
|
|
38
|
+
if (credentials.apiUrl && credentials.apiKey) {
|
|
39
|
+
console.log(chalk.gray('\nTesting API connection...'));
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
|
|
43
|
+
const status = await client.testConnection();
|
|
44
|
+
console.log(chalk.green(`✓ Connected to Continum API`));
|
|
45
|
+
console.log(chalk.gray(` Customer: ${status.customer}`));
|
|
46
|
+
console.log(chalk.gray(` Endpoint: ${credentials.apiUrl}`));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.log(chalk.red('✗ Failed to connect to API'));
|
|
49
|
+
console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
console.log(chalk.yellow('\n⚠️ No API credentials configured'));
|
|
53
|
+
console.log(chalk.gray(' Run `continum init` to set up'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log();
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { uninstallPreCommitHook, isGitRepository, getGitRoot } from '../git/git-utils';
|
|
6
|
+
|
|
7
|
+
export async function uninstallCommand(): Promise<void> {
|
|
8
|
+
console.log(chalk.blue.bold('\n🛡️ Uninstall Continum\n'));
|
|
9
|
+
|
|
10
|
+
if (!isGitRepository()) {
|
|
11
|
+
console.log(chalk.red('✗ Not in a git repository'));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { confirm } = await prompts({
|
|
16
|
+
type: 'confirm',
|
|
17
|
+
name: 'confirm',
|
|
18
|
+
message: 'Remove Continum pre-commit hook from this repository?',
|
|
19
|
+
initial: false
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!confirm) {
|
|
23
|
+
console.log(chalk.gray('Cancelled'));
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
uninstallPreCommitHook();
|
|
29
|
+
console.log(chalk.green('✓ Pre-commit hook removed'));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.log(chalk.red('✗ Failed to remove hook'));
|
|
32
|
+
console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Ask about config file
|
|
37
|
+
const gitRoot = getGitRoot();
|
|
38
|
+
const configPath = path.join(gitRoot, '.continum.json');
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(configPath)) {
|
|
41
|
+
const { removeConfig } = await prompts({
|
|
42
|
+
type: 'confirm',
|
|
43
|
+
name: 'removeConfig',
|
|
44
|
+
message: 'Also remove .continum.json config file?',
|
|
45
|
+
initial: false
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (removeConfig) {
|
|
49
|
+
fs.unlinkSync(configPath);
|
|
50
|
+
console.log(chalk.green('✓ Config file removed'));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.blue('\n✓ Continum uninstalled\n'));
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ContinumConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_CONFIG: ContinumConfig = {
|
|
4
|
+
scanOnCommit: true,
|
|
5
|
+
sandbox: 'default',
|
|
6
|
+
block: ['CRITICAL', 'HIGH'],
|
|
7
|
+
warn: ['MEDIUM'],
|
|
8
|
+
ignore: [
|
|
9
|
+
'.env.example',
|
|
10
|
+
'**/*.test.ts',
|
|
11
|
+
'**/*.test.js',
|
|
12
|
+
'**/*.spec.ts',
|
|
13
|
+
'**/*.spec.js',
|
|
14
|
+
'**/fixtures/**',
|
|
15
|
+
'**/mocks/**',
|
|
16
|
+
'**/test/**',
|
|
17
|
+
'**/tests/**',
|
|
18
|
+
'**/__tests__/**'
|
|
19
|
+
],
|
|
20
|
+
patterns: {
|
|
21
|
+
custom: []
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { ContinumConfig } from '../types';
|
|
5
|
+
import { DEFAULT_CONFIG } from './default-config';
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = '.continum.json';
|
|
8
|
+
const CACHE_DIR = path.join(os.homedir(), '.continum');
|
|
9
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'patterns.json');
|
|
10
|
+
const CREDENTIALS_FILE = path.join(CACHE_DIR, 'credentials.json');
|
|
11
|
+
|
|
12
|
+
export function loadConfig(cwd: string = process.cwd()): ContinumConfig {
|
|
13
|
+
const configPath = path.join(cwd, CONFIG_FILE);
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(configPath)) {
|
|
16
|
+
return DEFAULT_CONFIG;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
21
|
+
const config = JSON.parse(configContent);
|
|
22
|
+
return { ...DEFAULT_CONFIG, ...config };
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error loading config file, using defaults');
|
|
25
|
+
return DEFAULT_CONFIG;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function saveConfig(config: ContinumConfig, cwd: string = process.cwd()): void {
|
|
30
|
+
const configPath = path.join(cwd, CONFIG_FILE);
|
|
31
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadCredentials(): { apiUrl?: string; apiKey?: string } {
|
|
35
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const content = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function saveCredentials(apiUrl: string, apiKey: string): void {
|
|
48
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
49
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
CREDENTIALS_FILE,
|
|
54
|
+
JSON.stringify({ apiUrl, apiKey }, null, 2)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getCacheDir(): string {
|
|
59
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
60
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
return CACHE_DIR;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getCacheFile(): string {
|
|
66
|
+
return CACHE_FILE;
|
|
67
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
export function getStagedFiles(): string[] {
|
|
6
|
+
try {
|
|
7
|
+
const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
8
|
+
encoding: 'utf-8'
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return output
|
|
12
|
+
.split('\n')
|
|
13
|
+
.filter(file => file.trim() !== '')
|
|
14
|
+
.map(file => file.trim());
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error('Failed to get staged files. Are you in a git repository?');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getStagedDiff(): string {
|
|
21
|
+
try {
|
|
22
|
+
return execSync('git diff --cached', { encoding: 'utf-8' });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error('Failed to get staged diff');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isGitRepository(): boolean {
|
|
29
|
+
try {
|
|
30
|
+
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
31
|
+
return true;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getGitRoot(): string {
|
|
38
|
+
try {
|
|
39
|
+
const output = execSync('git rev-parse --show-toplevel', {
|
|
40
|
+
encoding: 'utf-8'
|
|
41
|
+
});
|
|
42
|
+
return output.trim();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error('Not in a git repository');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function installPreCommitHook(): void {
|
|
49
|
+
const gitRoot = getGitRoot();
|
|
50
|
+
const hookPath = path.join(gitRoot, '.git', 'hooks', 'pre-commit');
|
|
51
|
+
|
|
52
|
+
const hookContent = `#!/bin/sh
|
|
53
|
+
# Continum pre-commit hook
|
|
54
|
+
# This hook runs the Continum CLI scanner before each commit
|
|
55
|
+
|
|
56
|
+
npx @continum/cli scan --staged --hook
|
|
57
|
+
|
|
58
|
+
# Exit with the same code as the scanner
|
|
59
|
+
exit $?
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function uninstallPreCommitHook(): void {
|
|
66
|
+
const gitRoot = getGitRoot();
|
|
67
|
+
const hookPath = path.join(gitRoot, '.git', 'hooks', 'pre-commit');
|
|
68
|
+
|
|
69
|
+
if (fs.existsSync(hookPath)) {
|
|
70
|
+
const content = fs.readFileSync(hookPath, 'utf-8');
|
|
71
|
+
|
|
72
|
+
// Only remove if it's our hook
|
|
73
|
+
if (content.includes('Continum pre-commit hook')) {
|
|
74
|
+
fs.unlinkSync(hookPath);
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error('Pre-commit hook exists but is not a Continum hook. Remove manually.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function hasPreCommitHook(): boolean {
|
|
82
|
+
try {
|
|
83
|
+
const gitRoot = getGitRoot();
|
|
84
|
+
const hookPath = path.join(gitRoot, '.git', 'hooks', 'pre-commit');
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(hookPath)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const content = fs.readFileSync(hookPath, 'utf-8');
|
|
91
|
+
return content.includes('Continum pre-commit hook');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { loginCommand } from './commands/login';
|
|
5
|
+
import { initCommand } from './commands/init';
|
|
6
|
+
import { scanCommand } from './commands/scan';
|
|
7
|
+
import { patternsUpdateCommand, patternsListCommand } from './commands/patterns';
|
|
8
|
+
import { statusCommand } from './commands/status';
|
|
9
|
+
import { uninstallCommand } from './commands/uninstall';
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('continum')
|
|
15
|
+
.description('Continum CLI - Pre-commit credential scanner and pattern learning tool')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
|
|
18
|
+
// Login command
|
|
19
|
+
program
|
|
20
|
+
.command('login')
|
|
21
|
+
.description('Authenticate with Continum (opens browser)')
|
|
22
|
+
.action(loginCommand);
|
|
23
|
+
|
|
24
|
+
// Init command
|
|
25
|
+
program
|
|
26
|
+
.command('init')
|
|
27
|
+
.description('Initialize Continum in a project (creates config, installs git hook)')
|
|
28
|
+
.option('--silent', 'Run without prompts (for postinstall scripts)')
|
|
29
|
+
.action(initCommand);
|
|
30
|
+
|
|
31
|
+
// Scan command
|
|
32
|
+
program
|
|
33
|
+
.command('scan')
|
|
34
|
+
.description('Scan files for credentials and sensitive data')
|
|
35
|
+
.option('--staged', 'Scan staged files (for pre-commit hook)')
|
|
36
|
+
.option('--hook', 'Running from git hook (minimal output)')
|
|
37
|
+
.option('--strict', 'Block on unknown patterns without prompting')
|
|
38
|
+
.option('--auto-approve', 'Automatically approve unknown patterns')
|
|
39
|
+
.option('--warn-only', 'Show warnings but don\'t block commits')
|
|
40
|
+
.argument('[files...]', 'Files to scan')
|
|
41
|
+
.action((files: string[], options: any) => {
|
|
42
|
+
scanCommand({ ...options, files });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Patterns commands
|
|
46
|
+
const patterns = program
|
|
47
|
+
.command('patterns')
|
|
48
|
+
.description('Manage credential patterns');
|
|
49
|
+
|
|
50
|
+
patterns
|
|
51
|
+
.command('update')
|
|
52
|
+
.description('Update patterns from Continum API')
|
|
53
|
+
.action(patternsUpdateCommand);
|
|
54
|
+
|
|
55
|
+
patterns
|
|
56
|
+
.command('list')
|
|
57
|
+
.description('List all available patterns')
|
|
58
|
+
.action(patternsListCommand);
|
|
59
|
+
|
|
60
|
+
// Status command
|
|
61
|
+
program
|
|
62
|
+
.command('status')
|
|
63
|
+
.description('Check Continum configuration and connection')
|
|
64
|
+
.action(statusCommand);
|
|
65
|
+
|
|
66
|
+
// Uninstall command
|
|
67
|
+
program
|
|
68
|
+
.command('uninstall')
|
|
69
|
+
.description('Remove Continum pre-commit hook')
|
|
70
|
+
.action(uninstallCommand);
|
|
71
|
+
|
|
72
|
+
program.parse(process.argv);
|