surfscan 0.0.0-stage → 0.1.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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +188 -2
- package/dist/chunk-MJKZGRUS.js +2902 -0
- package/dist/chunk-MJKZGRUS.js.map +1 -0
- package/dist/cli/bin.js +514 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/index.d.ts +294 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/docs/rules.md +514 -0
- package/package.json +64 -4
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
type Severity = 'high' | 'medium' | 'low' | 'info';
|
|
2
|
+
/** Highest first. */
|
|
3
|
+
declare const SEVERITIES: readonly Severity[];
|
|
4
|
+
declare const SEVERITY_RANK: Record<Severity, number>;
|
|
5
|
+
declare function isSeverity(value: unknown): value is Severity;
|
|
6
|
+
interface Finding {
|
|
7
|
+
ruleId: string;
|
|
8
|
+
severity: Severity;
|
|
9
|
+
/** What was found. Must never contain a full secret value. */
|
|
10
|
+
message: string;
|
|
11
|
+
/** Path relative to the scan root, always with forward slashes. */
|
|
12
|
+
file: string;
|
|
13
|
+
/** 1-based. 0 means the finding applies to the whole file. */
|
|
14
|
+
line: number;
|
|
15
|
+
/** 1-based. */
|
|
16
|
+
column?: number;
|
|
17
|
+
/** How to fix it. */
|
|
18
|
+
fix: string;
|
|
19
|
+
}
|
|
20
|
+
interface SurfscanConfig {
|
|
21
|
+
/** Rule IDs to skip. */
|
|
22
|
+
ignore: string[];
|
|
23
|
+
/** Globs (relative to the scan root) to exclude from discovery. */
|
|
24
|
+
ignorePaths: string[];
|
|
25
|
+
deps: {
|
|
26
|
+
minAgeDays: number;
|
|
27
|
+
trustedPackages: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read-only access to the project being scanned. Implemented for Node in
|
|
32
|
+
* `node-provider.ts`; a browser demo or test can supply an in-memory one.
|
|
33
|
+
*/
|
|
34
|
+
interface SourceProvider {
|
|
35
|
+
/** List files matching the globs, relative to the root, with forward slashes. */
|
|
36
|
+
list(patterns: string[], ignore: string[]): Promise<string[]>;
|
|
37
|
+
/** Read a file relative to the root. Resolves to undefined if it does not exist. */
|
|
38
|
+
read(path: string, opts?: {
|
|
39
|
+
maxBytes?: number;
|
|
40
|
+
}): Promise<string | undefined>;
|
|
41
|
+
/** Links matched since the last call that were not scanned. Each is returned once. */
|
|
42
|
+
takeSkippedLinks?(): string[];
|
|
43
|
+
}
|
|
44
|
+
interface SourceFile {
|
|
45
|
+
/** Relative to the scan root, forward slashes. */
|
|
46
|
+
path: string;
|
|
47
|
+
content: string;
|
|
48
|
+
}
|
|
49
|
+
interface ScanContext {
|
|
50
|
+
files: SourceFile[];
|
|
51
|
+
config: SurfscanConfig;
|
|
52
|
+
provider: SourceProvider;
|
|
53
|
+
/** Scanner-specific options (for example `offline` for deps). */
|
|
54
|
+
options: Readonly<Record<string, unknown>>;
|
|
55
|
+
/**
|
|
56
|
+
* Report a non-fatal problem, such as an unreadable file or a network failure.
|
|
57
|
+
* Pass `incomplete: true` when the problem meant a check could not run, so
|
|
58
|
+
* the result cannot be read as "nothing found".
|
|
59
|
+
*/
|
|
60
|
+
warn(message: string, opts?: {
|
|
61
|
+
incomplete?: boolean;
|
|
62
|
+
}): void;
|
|
63
|
+
}
|
|
64
|
+
interface Rule {
|
|
65
|
+
id: string;
|
|
66
|
+
severity: Severity;
|
|
67
|
+
title: string;
|
|
68
|
+
check(context: ScanContext): Finding[] | Promise<Finding[]>;
|
|
69
|
+
}
|
|
70
|
+
interface Scanner {
|
|
71
|
+
name: string;
|
|
72
|
+
/** Ordered high to low severity. */
|
|
73
|
+
rules: Rule[];
|
|
74
|
+
findTargets(provider: SourceProvider, config: SurfscanConfig): Promise<string[]>;
|
|
75
|
+
/** Read limit for this scanner's files, when they are routinely larger than the default. */
|
|
76
|
+
maxFileBytes?: number;
|
|
77
|
+
}
|
|
78
|
+
interface ScanResult {
|
|
79
|
+
findings: Finding[];
|
|
80
|
+
warnings: string[];
|
|
81
|
+
/** True when at least one check could not run (for example the registry was unreachable). */
|
|
82
|
+
incomplete: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Paths never scanned, in addition to config.ignorePaths. */
|
|
86
|
+
declare const DEFAULT_IGNORE_PATHS: string[];
|
|
87
|
+
interface RunOptions {
|
|
88
|
+
provider: SourceProvider;
|
|
89
|
+
config: SurfscanConfig;
|
|
90
|
+
/** Scanner-specific options, passed through to rules as `context.options`. */
|
|
91
|
+
options?: Record<string, unknown>;
|
|
92
|
+
}
|
|
93
|
+
declare function compareFindings(a: Finding, b: Finding): number;
|
|
94
|
+
declare function sortFindings(findings: Finding[]): Finding[];
|
|
95
|
+
/** Discover target files for a scanner, honouring default and configured ignores. */
|
|
96
|
+
declare function listTargets(provider: SourceProvider, config: SurfscanConfig, patterns: string[], extraIgnore?: string[]): Promise<string[]>;
|
|
97
|
+
declare function runScanner(scanner: Scanner, opts: RunOptions): Promise<ScanResult>;
|
|
98
|
+
declare function runScanners(scanners: Scanner[], opts: RunOptions): Promise<ScanResult>;
|
|
99
|
+
declare function summarize(findings: Finding[]): Record<Severity, number>;
|
|
100
|
+
/** True when at least one finding is at or above the threshold. */
|
|
101
|
+
declare function hasFailures(findings: Finding[], failOn: Severity): boolean;
|
|
102
|
+
|
|
103
|
+
declare const CONFIG_FILE_NAME = "surfscan.json";
|
|
104
|
+
declare const DEFAULT_CONFIG: SurfscanConfig;
|
|
105
|
+
declare class ConfigError extends Error {
|
|
106
|
+
constructor(message: string);
|
|
107
|
+
}
|
|
108
|
+
/** Validate a parsed config object and merge it over the defaults. */
|
|
109
|
+
declare function validateConfig(raw: unknown, source?: string): SurfscanConfig;
|
|
110
|
+
/** Parse and validate the text of a surfscan.json file. */
|
|
111
|
+
declare function parseConfig(text: string, source?: string): SurfscanConfig;
|
|
112
|
+
/**
|
|
113
|
+
* Load a config file through the given reader. Resolves to the defaults when
|
|
114
|
+
* the file does not exist and `required` is false.
|
|
115
|
+
*/
|
|
116
|
+
declare function loadConfig(read: (path: string) => Promise<string | undefined>, path: string, required: boolean): Promise<SurfscanConfig>;
|
|
117
|
+
|
|
118
|
+
/** Read-only file access rooted at a directory on disk. */
|
|
119
|
+
declare function createNodeProvider(root: string): SourceProvider;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Patterns for well-known secret formats. Used by the report layer as a last
|
|
123
|
+
* line of defence against printing a secret, and by the keys scanner.
|
|
124
|
+
*/
|
|
125
|
+
interface SecretPattern {
|
|
126
|
+
name: string;
|
|
127
|
+
regex: RegExp;
|
|
128
|
+
}
|
|
129
|
+
declare const SECRET_PATTERNS: readonly SecretPattern[];
|
|
130
|
+
/** Mask a secret value: first 4 characters, then "...". */
|
|
131
|
+
declare function maskSecret(value: string): string;
|
|
132
|
+
/** Replace anything that looks like a secret in text about to be printed. */
|
|
133
|
+
declare function redactSecrets(text: string): string;
|
|
134
|
+
|
|
135
|
+
interface TextOptions {
|
|
136
|
+
color: boolean;
|
|
137
|
+
/** A check could not run; say so under the summary. */
|
|
138
|
+
incomplete?: boolean;
|
|
139
|
+
}
|
|
140
|
+
declare function summaryLine(findings: Finding[]): string;
|
|
141
|
+
/** Findings must already be sorted. */
|
|
142
|
+
declare function formatText(findings: Finding[], opts: TextOptions): string;
|
|
143
|
+
|
|
144
|
+
declare const JSON_REPORT_VERSION = 1;
|
|
145
|
+
interface JsonReport {
|
|
146
|
+
version: number;
|
|
147
|
+
summary: Record<Severity, number>;
|
|
148
|
+
/** False when a check could not run, so an empty `findings` does not mean clean. */
|
|
149
|
+
complete: boolean;
|
|
150
|
+
warnings: string[];
|
|
151
|
+
findings: Finding[];
|
|
152
|
+
}
|
|
153
|
+
interface ReportMeta {
|
|
154
|
+
warnings?: string[];
|
|
155
|
+
incomplete?: boolean;
|
|
156
|
+
}
|
|
157
|
+
declare function toJsonReport(findings: Finding[], meta?: ReportMeta): JsonReport;
|
|
158
|
+
declare function formatJson(findings: Finding[], meta?: ReportMeta): string;
|
|
159
|
+
|
|
160
|
+
/** Make text safe to print on a terminal: escape control and invisible characters. */
|
|
161
|
+
declare function escapeUnsafe(text: string): string;
|
|
162
|
+
/** Mask secrets in every text field of a finding. Applied to all output formats. */
|
|
163
|
+
declare function redactFinding(f: Finding): Finding;
|
|
164
|
+
|
|
165
|
+
/** Agent instruction files, at any depth so monorepo packages are covered. */
|
|
166
|
+
declare const RULES_TARGETS: string[];
|
|
167
|
+
declare const rules$4: Rule[];
|
|
168
|
+
declare const rulesScanner: Scanner;
|
|
169
|
+
|
|
170
|
+
/** A match inside some text, before it is mapped to a file position. */
|
|
171
|
+
interface TextMatch {
|
|
172
|
+
index: number;
|
|
173
|
+
/** Short human-readable description of what matched. */
|
|
174
|
+
detail: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
declare const KEYS_TARGETS: string[];
|
|
178
|
+
declare const rules$3: Rule[];
|
|
179
|
+
declare const keysScanner: Scanner;
|
|
180
|
+
|
|
181
|
+
/** A setting found in an agent config, with the line it is on. */
|
|
182
|
+
interface Located<T> {
|
|
183
|
+
value: T;
|
|
184
|
+
line: number;
|
|
185
|
+
}
|
|
186
|
+
/** One auto-approve entry, normalized across agents. */
|
|
187
|
+
interface AllowEntry {
|
|
188
|
+
/** The entry as written, for messages. */
|
|
189
|
+
raw: string;
|
|
190
|
+
line: number;
|
|
191
|
+
/** 'shell' covers Bash, PowerShell and similar command tools. */
|
|
192
|
+
kind: 'shell' | 'mcp' | 'other';
|
|
193
|
+
/** Shell: the command pattern (`*` is a wildcard). Undefined means every command. */
|
|
194
|
+
command?: string;
|
|
195
|
+
/** MCP: server name, and tool pattern (undefined means every tool). */
|
|
196
|
+
mcpServer?: string;
|
|
197
|
+
mcpTool?: string;
|
|
198
|
+
}
|
|
199
|
+
/** What an agent config auto-approves, in agent-neutral terms. */
|
|
200
|
+
interface ApprovalSettings {
|
|
201
|
+
agent: string;
|
|
202
|
+
file: string;
|
|
203
|
+
/** Modes or flags that skip permission prompts entirely. */
|
|
204
|
+
bypass: Array<Located<string>>;
|
|
205
|
+
allow: AllowEntry[];
|
|
206
|
+
/** Modes that auto-approve some actions, such as file edits. */
|
|
207
|
+
partial: Array<Located<string>>;
|
|
208
|
+
/** Settings that auto-approve every project MCP server. */
|
|
209
|
+
allMcpServers: Array<Located<string>>;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Reads one agent's config format. Add an adapter per agent; rules only see
|
|
213
|
+
* ApprovalSettings.
|
|
214
|
+
*/
|
|
215
|
+
interface ApprovalAdapter {
|
|
216
|
+
agent: string;
|
|
217
|
+
/** Globs relative to the project root. */
|
|
218
|
+
targets: string[];
|
|
219
|
+
/** True when a discovered path belongs to this adapter. */
|
|
220
|
+
matches(path: string): boolean;
|
|
221
|
+
/** Returns undefined when the file holds nothing to check. Throws on parse errors. */
|
|
222
|
+
parse(file: SourceFile): ApprovalSettings | undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Registered agent adapters. Add new agents here. */
|
|
226
|
+
declare const adapters: ApprovalAdapter[];
|
|
227
|
+
declare const rules$2: Rule[];
|
|
228
|
+
declare const approvalsScanner: Scanner;
|
|
229
|
+
|
|
230
|
+
declare const MCP_TARGETS: string[];
|
|
231
|
+
declare const rules$1: Rule[];
|
|
232
|
+
declare const mcpScanner: Scanner;
|
|
233
|
+
|
|
234
|
+
declare const LOCK_FILE_PATH = ".surfscan/mcp.lock.json";
|
|
235
|
+
interface McpTool {
|
|
236
|
+
name: string;
|
|
237
|
+
/** The tool as exported: description, inputSchema and so on. */
|
|
238
|
+
definition: Record<string, unknown>;
|
|
239
|
+
}
|
|
240
|
+
type ToolList = Record<string, McpTool[]>;
|
|
241
|
+
/** Options read from `ScanContext.options`. */
|
|
242
|
+
interface ToolSource {
|
|
243
|
+
/** Where the list came from, shown in findings. Omitted when read from running servers. */
|
|
244
|
+
file?: string;
|
|
245
|
+
/** The text of the file, used to find line numbers. */
|
|
246
|
+
content?: string;
|
|
247
|
+
tools: ToolList;
|
|
248
|
+
}
|
|
249
|
+
type LockFile = {
|
|
250
|
+
version: 1;
|
|
251
|
+
servers: Record<string, Record<string, string>>;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Accepts a `tools/list` result (`{ "tools": [...] }`, optionally inside a
|
|
255
|
+
* JSON-RPC `result`), a bare array, or `{ "servers": { "<name>": <either> } }`.
|
|
256
|
+
*/
|
|
257
|
+
declare function parseToolList(text: string): ToolList;
|
|
258
|
+
declare function buildLock(tools: ToolList): Promise<LockFile>;
|
|
259
|
+
|
|
260
|
+
type FetchLike = (url: string, init?: {
|
|
261
|
+
headers?: Record<string, string>;
|
|
262
|
+
signal?: AbortSignal;
|
|
263
|
+
}) => Promise<{
|
|
264
|
+
ok: boolean;
|
|
265
|
+
status: number;
|
|
266
|
+
json(): Promise<unknown>;
|
|
267
|
+
}>;
|
|
268
|
+
|
|
269
|
+
declare const DEPS_TARGETS: string[];
|
|
270
|
+
/** Options read from `ScanContext.options`. */
|
|
271
|
+
interface DepsOptions {
|
|
272
|
+
offline?: boolean;
|
|
273
|
+
fetch?: FetchLike;
|
|
274
|
+
registryUrl?: string;
|
|
275
|
+
downloadsUrl?: string;
|
|
276
|
+
}
|
|
277
|
+
declare const rules: Rule[];
|
|
278
|
+
declare const depsScanner: Scanner;
|
|
279
|
+
|
|
280
|
+
interface InvisibleMatch extends TextMatch {
|
|
281
|
+
codePoints: number[];
|
|
282
|
+
}
|
|
283
|
+
/** One match per line, listing the distinct invisible characters found on it. */
|
|
284
|
+
declare function findInvisibleChars(text: string): InvisibleMatch[];
|
|
285
|
+
declare function findHiddenContent(text: string): TextMatch[];
|
|
286
|
+
declare function findPipeToShell(text: string): TextMatch[];
|
|
287
|
+
declare function findCredentialAccess(text: string): TextMatch[];
|
|
288
|
+
declare function findInstructionOverride(text: string): TextMatch[];
|
|
289
|
+
declare function findEncodedBlocks(text: string): TextMatch[];
|
|
290
|
+
declare function findUnpinnedInstalls(text: string): TextMatch[];
|
|
291
|
+
declare function findRemoteInstructions(text: string): TextMatch[];
|
|
292
|
+
|
|
293
|
+
export { CONFIG_FILE_NAME, ConfigError, DEFAULT_CONFIG, DEFAULT_IGNORE_PATHS, DEPS_TARGETS, JSON_REPORT_VERSION, KEYS_TARGETS, LOCK_FILE_PATH, MCP_TARGETS, RULES_TARGETS, SECRET_PATTERNS, SEVERITIES, SEVERITY_RANK, adapters as approvalAdapters, rules$2 as approvalRules, approvalsScanner, buildLock, compareFindings, createNodeProvider, rules as depsRules, depsScanner, escapeUnsafe, findCredentialAccess, findEncodedBlocks, findHiddenContent, findInstructionOverride, findInvisibleChars, findPipeToShell, findRemoteInstructions, findUnpinnedInstalls, formatJson, formatText, hasFailures, rules$4 as instructionRules, isSeverity, rules$3 as keyRules, keysScanner, listTargets, loadConfig, maskSecret, rules$1 as mcpRules, mcpScanner, parseConfig, parseToolList, redactFinding, redactSecrets, rulesScanner, runScanner, runScanners, sortFindings, summarize, summaryLine, toJsonReport, validateConfig };
|
|
294
|
+
export type { AllowEntry, ApprovalAdapter, ApprovalSettings, DepsOptions, FetchLike, Finding, JsonReport, LockFile, McpTool, Rule, RunOptions, ScanContext, ScanResult, Scanner, Severity, SourceFile, SourceProvider, SurfscanConfig, TextOptions, ToolList, ToolSource };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_FILE_NAME,
|
|
3
|
+
ConfigError,
|
|
4
|
+
DEFAULT_CONFIG,
|
|
5
|
+
DEFAULT_IGNORE_PATHS,
|
|
6
|
+
DEPS_TARGETS,
|
|
7
|
+
JSON_REPORT_VERSION,
|
|
8
|
+
KEYS_TARGETS,
|
|
9
|
+
LOCK_FILE_PATH,
|
|
10
|
+
MCP_TARGETS,
|
|
11
|
+
RULES_TARGETS,
|
|
12
|
+
SECRET_PATTERNS,
|
|
13
|
+
SEVERITIES,
|
|
14
|
+
SEVERITY_RANK,
|
|
15
|
+
adapters,
|
|
16
|
+
approvalsScanner,
|
|
17
|
+
buildLock,
|
|
18
|
+
compareFindings,
|
|
19
|
+
createNodeProvider,
|
|
20
|
+
depsScanner,
|
|
21
|
+
escapeUnsafe,
|
|
22
|
+
findCredentialAccess,
|
|
23
|
+
findEncodedBlocks,
|
|
24
|
+
findHiddenContent,
|
|
25
|
+
findInstructionOverride,
|
|
26
|
+
findInvisibleChars,
|
|
27
|
+
findPipeToShell,
|
|
28
|
+
findRemoteInstructions,
|
|
29
|
+
findUnpinnedInstalls,
|
|
30
|
+
formatJson,
|
|
31
|
+
formatText,
|
|
32
|
+
hasFailures,
|
|
33
|
+
isSeverity,
|
|
34
|
+
keysScanner,
|
|
35
|
+
listTargets,
|
|
36
|
+
loadConfig,
|
|
37
|
+
maskSecret,
|
|
38
|
+
mcpScanner,
|
|
39
|
+
parseConfig,
|
|
40
|
+
parseToolList,
|
|
41
|
+
redactFinding,
|
|
42
|
+
redactSecrets,
|
|
43
|
+
rules,
|
|
44
|
+
rules2,
|
|
45
|
+
rules3,
|
|
46
|
+
rules4,
|
|
47
|
+
rules5,
|
|
48
|
+
rulesScanner,
|
|
49
|
+
runScanner,
|
|
50
|
+
runScanners,
|
|
51
|
+
sortFindings,
|
|
52
|
+
summarize,
|
|
53
|
+
summaryLine,
|
|
54
|
+
toJsonReport,
|
|
55
|
+
validateConfig
|
|
56
|
+
} from "./chunk-MJKZGRUS.js";
|
|
57
|
+
export {
|
|
58
|
+
CONFIG_FILE_NAME,
|
|
59
|
+
ConfigError,
|
|
60
|
+
DEFAULT_CONFIG,
|
|
61
|
+
DEFAULT_IGNORE_PATHS,
|
|
62
|
+
DEPS_TARGETS,
|
|
63
|
+
JSON_REPORT_VERSION,
|
|
64
|
+
KEYS_TARGETS,
|
|
65
|
+
LOCK_FILE_PATH,
|
|
66
|
+
MCP_TARGETS,
|
|
67
|
+
RULES_TARGETS,
|
|
68
|
+
SECRET_PATTERNS,
|
|
69
|
+
SEVERITIES,
|
|
70
|
+
SEVERITY_RANK,
|
|
71
|
+
adapters as approvalAdapters,
|
|
72
|
+
rules3 as approvalRules,
|
|
73
|
+
approvalsScanner,
|
|
74
|
+
buildLock,
|
|
75
|
+
compareFindings,
|
|
76
|
+
createNodeProvider,
|
|
77
|
+
rules5 as depsRules,
|
|
78
|
+
depsScanner,
|
|
79
|
+
escapeUnsafe,
|
|
80
|
+
findCredentialAccess,
|
|
81
|
+
findEncodedBlocks,
|
|
82
|
+
findHiddenContent,
|
|
83
|
+
findInstructionOverride,
|
|
84
|
+
findInvisibleChars,
|
|
85
|
+
findPipeToShell,
|
|
86
|
+
findRemoteInstructions,
|
|
87
|
+
findUnpinnedInstalls,
|
|
88
|
+
formatJson,
|
|
89
|
+
formatText,
|
|
90
|
+
hasFailures,
|
|
91
|
+
rules as instructionRules,
|
|
92
|
+
isSeverity,
|
|
93
|
+
rules2 as keyRules,
|
|
94
|
+
keysScanner,
|
|
95
|
+
listTargets,
|
|
96
|
+
loadConfig,
|
|
97
|
+
maskSecret,
|
|
98
|
+
rules4 as mcpRules,
|
|
99
|
+
mcpScanner,
|
|
100
|
+
parseConfig,
|
|
101
|
+
parseToolList,
|
|
102
|
+
redactFinding,
|
|
103
|
+
redactSecrets,
|
|
104
|
+
rulesScanner,
|
|
105
|
+
runScanner,
|
|
106
|
+
runScanners,
|
|
107
|
+
sortFindings,
|
|
108
|
+
summarize,
|
|
109
|
+
summaryLine,
|
|
110
|
+
toJsonReport,
|
|
111
|
+
validateConfig
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|