ship-safe 5.0.0 → 5.0.1
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/cli/agents/base-agent.js +2 -1
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/commands/agent.js +2 -0
- package/cli/commands/audit.js +71 -7
- package/cli/commands/baseline.js +2 -1
- package/cli/commands/ci.js +262 -260
- package/cli/commands/fix.js +218 -216
- package/cli/commands/mcp.js +304 -303
- package/cli/commands/remediate.js +2 -0
- package/cli/commands/scan.js +567 -565
- package/cli/commands/score.js +2 -0
- package/cli/commands/watch.js +161 -160
- package/cli/index.js +1 -1
- package/cli/utils/patterns.js +1118 -1104
- package/package.json +1 -1
package/cli/commands/score.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
SECURITY_PATTERNS,
|
|
37
37
|
SKIP_DIRS,
|
|
38
38
|
SKIP_EXTENSIONS,
|
|
39
|
+
SKIP_FILENAMES,
|
|
39
40
|
TEST_FILE_PATTERNS,
|
|
40
41
|
MAX_FILE_SIZE
|
|
41
42
|
} from '../utils/patterns.js';
|
|
@@ -335,6 +336,7 @@ async function findFiles(rootPath) {
|
|
|
335
336
|
for (const file of files) {
|
|
336
337
|
const ext = path.extname(file).toLowerCase();
|
|
337
338
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
339
|
+
if (SKIP_FILENAMES.has(path.basename(file))) continue;
|
|
338
340
|
|
|
339
341
|
const basename = path.basename(file);
|
|
340
342
|
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) continue;
|
package/cli/commands/watch.js
CHANGED
|
@@ -1,160 +1,161 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watch Command
|
|
3
|
-
* ==============
|
|
4
|
-
*
|
|
5
|
-
* Continuous file monitoring mode. Watches for file changes
|
|
6
|
-
* and incrementally scans modified files.
|
|
7
|
-
*
|
|
8
|
-
* USAGE:
|
|
9
|
-
* npx ship-safe watch [path] Start watching for changes
|
|
10
|
-
* npx ship-safe watch . --poll Use polling (for network drives)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
import chalk from 'chalk';
|
|
16
|
-
import { SKIP_DIRS, SKIP_EXTENSIONS, SECRET_PATTERNS, SECURITY_PATTERNS } from '../utils/patterns.js';
|
|
17
|
-
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
18
|
-
import * as output from '../utils/output.js';
|
|
19
|
-
|
|
20
|
-
export async function watchCommand(targetPath = '.', options = {}) {
|
|
21
|
-
const absolutePath = path.resolve(targetPath);
|
|
22
|
-
|
|
23
|
-
if (!fs.existsSync(absolutePath)) {
|
|
24
|
-
output.error(`Path does not exist: ${absolutePath}`);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
console.log();
|
|
29
|
-
output.header('Ship Safe — Watch Mode');
|
|
30
|
-
console.log();
|
|
31
|
-
console.log(chalk.cyan(' Watching for file changes...'));
|
|
32
|
-
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
33
|
-
console.log();
|
|
34
|
-
|
|
35
|
-
const allPatterns = [...SECRET_PATTERNS, ...SECURITY_PATTERNS];
|
|
36
|
-
const skipDirSet = SKIP_DIRS;
|
|
37
|
-
let debounceTimer = null;
|
|
38
|
-
const pendingFiles = new Set();
|
|
39
|
-
|
|
40
|
-
// Use fs.watch recursively
|
|
41
|
-
try {
|
|
42
|
-
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
43
|
-
if (!filename) return;
|
|
44
|
-
|
|
45
|
-
const fullPath = path.join(absolutePath, filename); // ship-safe-ignore — filename from fs.watch, not user input
|
|
46
|
-
const relPath = filename.replace(/\\/g, '/');
|
|
47
|
-
|
|
48
|
-
// Skip directories we don't care about
|
|
49
|
-
for (const skipDir of skipDirSet) {
|
|
50
|
-
if (relPath.includes(`${skipDir}/`) || relPath.startsWith(`${skipDir}/`)) return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Skip non-code files
|
|
54
|
-
const ext = path.extname(filename).toLowerCase();
|
|
55
|
-
if (SKIP_EXTENSIONS.has(ext)) return;
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
debounceTimer
|
|
63
|
-
|
|
64
|
-
pendingFiles
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
: chalk.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
chalk.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
seen.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Watch Command
|
|
3
|
+
* ==============
|
|
4
|
+
*
|
|
5
|
+
* Continuous file monitoring mode. Watches for file changes
|
|
6
|
+
* and incrementally scans modified files.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* npx ship-safe watch [path] Start watching for changes
|
|
10
|
+
* npx ship-safe watch . --poll Use polling (for network drives)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, SECRET_PATTERNS, SECURITY_PATTERNS } from '../utils/patterns.js';
|
|
17
|
+
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
18
|
+
import * as output from '../utils/output.js';
|
|
19
|
+
|
|
20
|
+
export async function watchCommand(targetPath = '.', options = {}) {
|
|
21
|
+
const absolutePath = path.resolve(targetPath);
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(absolutePath)) {
|
|
24
|
+
output.error(`Path does not exist: ${absolutePath}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log();
|
|
29
|
+
output.header('Ship Safe — Watch Mode');
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.cyan(' Watching for file changes...'));
|
|
32
|
+
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
const allPatterns = [...SECRET_PATTERNS, ...SECURITY_PATTERNS];
|
|
36
|
+
const skipDirSet = SKIP_DIRS;
|
|
37
|
+
let debounceTimer = null;
|
|
38
|
+
const pendingFiles = new Set();
|
|
39
|
+
|
|
40
|
+
// Use fs.watch recursively
|
|
41
|
+
try {
|
|
42
|
+
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
43
|
+
if (!filename) return;
|
|
44
|
+
|
|
45
|
+
const fullPath = path.join(absolutePath, filename); // ship-safe-ignore — filename from fs.watch, not user input
|
|
46
|
+
const relPath = filename.replace(/\\/g, '/');
|
|
47
|
+
|
|
48
|
+
// Skip directories we don't care about
|
|
49
|
+
for (const skipDir of skipDirSet) {
|
|
50
|
+
if (relPath.includes(`${skipDir}/`) || relPath.startsWith(`${skipDir}/`)) return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Skip non-code files
|
|
54
|
+
const ext = path.extname(filename).toLowerCase();
|
|
55
|
+
if (SKIP_EXTENSIONS.has(ext)) return;
|
|
56
|
+
if (SKIP_FILENAMES.has(path.basename(filename))) return;
|
|
57
|
+
if (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
|
|
58
|
+
|
|
59
|
+
// Add to pending and debounce
|
|
60
|
+
pendingFiles.add(fullPath);
|
|
61
|
+
|
|
62
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
63
|
+
debounceTimer = setTimeout(() => {
|
|
64
|
+
const filesToScan = [...pendingFiles];
|
|
65
|
+
pendingFiles.clear();
|
|
66
|
+
scanChangedFiles(filesToScan, allPatterns, absolutePath);
|
|
67
|
+
}, 300);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Keep the process alive
|
|
71
|
+
process.on('SIGINT', () => {
|
|
72
|
+
watcher.close();
|
|
73
|
+
console.log();
|
|
74
|
+
output.info('Watch mode stopped.');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Prevent Node from exiting
|
|
79
|
+
setInterval(() => {}, 1000 * 60 * 60);
|
|
80
|
+
|
|
81
|
+
} catch (err) {
|
|
82
|
+
output.error(`Watch failed: ${err.message}`);
|
|
83
|
+
console.log(chalk.gray(' Try: npx ship-safe watch . --poll'));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scanChangedFiles(files, patterns, rootPath) {
|
|
89
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
90
|
+
let totalFindings = 0;
|
|
91
|
+
|
|
92
|
+
for (const filePath of files) {
|
|
93
|
+
if (!fs.existsSync(filePath)) continue;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const stats = fs.statSync(filePath);
|
|
97
|
+
if (stats.size > 1_000_000) continue;
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const findings = scanFile(filePath, patterns);
|
|
103
|
+
if (findings.length > 0) {
|
|
104
|
+
totalFindings += findings.length;
|
|
105
|
+
const relPath = path.relative(rootPath, filePath);
|
|
106
|
+
|
|
107
|
+
for (const f of findings) {
|
|
108
|
+
const sevColor = f.severity === 'critical' ? chalk.red.bold
|
|
109
|
+
: f.severity === 'high' ? chalk.yellow
|
|
110
|
+
: chalk.blue;
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
chalk.gray(` [${timestamp}] `) +
|
|
114
|
+
sevColor(`[${f.severity.toUpperCase()}]`) +
|
|
115
|
+
chalk.white(` ${relPath}:${f.line} `) +
|
|
116
|
+
chalk.gray(f.patternName)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (totalFindings === 0 && files.length > 0) {
|
|
123
|
+
console.log(chalk.gray(` [${timestamp}] ${files.length} file(s) scanned — clean`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function scanFile(filePath, patterns) {
|
|
128
|
+
const findings = [];
|
|
129
|
+
try {
|
|
130
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < lines.length; i++) {
|
|
134
|
+
const line = lines[i];
|
|
135
|
+
if (/ship-safe-ignore/i.test(line)) continue;
|
|
136
|
+
|
|
137
|
+
for (const pattern of patterns) {
|
|
138
|
+
pattern.pattern.lastIndex = 0;
|
|
139
|
+
let match;
|
|
140
|
+
while ((match = pattern.pattern.exec(line)) !== null) {
|
|
141
|
+
if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
|
|
142
|
+
findings.push({
|
|
143
|
+
line: i + 1,
|
|
144
|
+
patternName: pattern.name,
|
|
145
|
+
severity: pattern.severity,
|
|
146
|
+
matched: match[0],
|
|
147
|
+
category: pattern.category || 'secret',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch { /* skip */ }
|
|
153
|
+
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
return findings.filter(f => {
|
|
156
|
+
const key = `${f.line}:${f.matched}`;
|
|
157
|
+
if (seen.has(key)) return false;
|
|
158
|
+
seen.add(key);
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export { doctorCommand } from './commands/doctor.js';
|
|
|
26
26
|
export { baselineCommand } from './commands/baseline.js';
|
|
27
27
|
|
|
28
28
|
// ── Patterns ──────────────────────────────────────────────────────────────────
|
|
29
|
-
export { SECRET_PATTERNS, SECURITY_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS } from './utils/patterns.js';
|
|
29
|
+
export { SECRET_PATTERNS, SECURITY_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES } from './utils/patterns.js';
|
|
30
30
|
|
|
31
31
|
// ── Agent Framework ───────────────────────────────────────────────────────────
|
|
32
32
|
export { BaseAgent, createFinding } from './agents/base-agent.js';
|