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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1 (2026-11-09)
|
|
4
|
+
|
|
5
|
+
- New `deps.trustedPackages` config key: DEPS-004 skips the listed packages and says which it skipped. surfscan itself is always skipped, so depending on it no longer raises DEPS-004.
|
|
6
|
+
|
|
7
|
+
## 0.1.0 (2026-11-05)
|
|
8
|
+
|
|
9
|
+
First release.
|
|
10
|
+
|
|
11
|
+
- Five scanners: `rules`, `mcp`, `deps`, `keys`, `approvals`, plus `scan` to run them all.
|
|
12
|
+
- 29 rules: RULES-001 to 008, MCP-001 to 006, DEPS-001 to 007, KEYS-001 to 003, APPR-001 to 005.
|
|
13
|
+
- `keys` recognises AWS, Stripe, GitHub, Slack, Anthropic, OpenAI, Google, npm, GitLab, Hugging Face and SendGrid keys, JSON Web Tokens and PEM private keys.
|
|
14
|
+
- Text and JSON reports. JSON includes `complete` and `warnings`, so an empty result from an incomplete scan is not mistaken for a clean one. Both mask secrets quoted from scanned files.
|
|
15
|
+
- Reads UTF-8 and UTF-16 files. Files too large to read, and links it will not follow, are reported rather than skipped in silence.
|
|
16
|
+
- `--fail-on` threshold and `--strict`, which exits 2 when a check could not run.
|
|
17
|
+
- `deps --offline` for local-only checks.
|
|
18
|
+
- `mcp --tools` to check an exported tool list, `--update-lock` to save description hashes, and opt-in `--connect` to list tools from local servers (per-server confirmation, minimal environment, unpinned servers need explicit approval).
|
|
19
|
+
- `surfscan.json` config with `ignore`, `ignorePaths` and `deps.minAgeDays`. A config in the scanned project that turns checks off is reported, and `--no-config` ignores it.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omogbai Atakpu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,189 @@
|
|
|
1
|
-
#
|
|
1
|
+
# surfscan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Attack surface scanner for AI-assisted development.
|
|
4
|
+
|
|
5
|
+
surfscan is a local, read-only CLI that checks a project for the ways AI coding agents widen its attack surface: what agents read (instruction files, MCP configs), what they install (dependencies), what they can reach (keys and secrets), and what they can run without asking (auto-approved commands).
|
|
6
|
+
|
|
7
|
+
Every finding has a rule ID, severity, file, line where one applies, and a fix hint. It works on a developer machine, in a pre-commit hook and in CI.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Requires Node 20 or later.
|
|
12
|
+
|
|
13
|
+
Run it once, without installing:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npx surfscan scan .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Install it globally:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
npm install -g surfscan
|
|
23
|
+
surfscan scan .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Add it to a project as a dev dependency:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
npm install --save-dev surfscan
|
|
30
|
+
npx surfscan scan .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`npx surfscan` fetches the latest release each time it runs. To keep results repeatable, pin a version, for example `npx surfscan@0.1.0 scan .`, or install it as a dev dependency and commit the lock file.
|
|
34
|
+
|
|
35
|
+
To build from source:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
git clone https://github.com/githero20/surfscan.git
|
|
39
|
+
cd surfscan
|
|
40
|
+
npm ci
|
|
41
|
+
npm run build
|
|
42
|
+
node dist/cli/bin.js scan .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### In CI or a pre-commit hook
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
npx surfscan@0.1.0 scan . --strict --fail-on high
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Exit code 1 means a finding at or above `--fail-on`. Exit code 2 means an error, or with `--strict`, a check that could not run. See [Exit codes](#exit-codes).
|
|
52
|
+
|
|
53
|
+
When CI scans pull requests, add `--no-config`, or pass your own `--config <file>` from outside the repo. A pull request can change the `surfscan.json` in the scanned directory, and that file can turn checks off.
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
Each command takes an optional path, which defaults to `.`.
|
|
58
|
+
|
|
59
|
+
| Command | What it does | Example |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `rules` | Checks agent instruction files for hidden or risky instructions | `surfscan rules .` |
|
|
62
|
+
| `mcp` | Audits MCP server configs and tool descriptions | `surfscan mcp --tools tools.json` |
|
|
63
|
+
| `deps` | Verifies dependencies against the npm registry | `surfscan deps --offline` |
|
|
64
|
+
| `keys` | Finds keys and secrets an agent can reach | `surfscan keys .` |
|
|
65
|
+
| `approvals` | Finds commands agents may run without asking | `surfscan approvals .` |
|
|
66
|
+
| `scan` | Runs all five and merges the results | `surfscan scan . --fail-on medium` |
|
|
67
|
+
|
|
68
|
+
Options for every command:
|
|
69
|
+
|
|
70
|
+
- `--format text|json`: output format (default `text`)
|
|
71
|
+
- `--fail-on high|medium|low|info`: exit 1 at or above this severity (default `high`)
|
|
72
|
+
- `--config <file>`: config file (default `surfscan.json` in the scanned directory, if present)
|
|
73
|
+
- `--no-config`: ignore any `surfscan.json` in the scanned directory
|
|
74
|
+
- `--no-color`: disable colored output
|
|
75
|
+
- `--strict`: exit 2 if any check could not run (see Exit codes)
|
|
76
|
+
|
|
77
|
+
Options for `deps` and `scan`:
|
|
78
|
+
|
|
79
|
+
- `--offline`: do not contact the registry; run local checks only
|
|
80
|
+
|
|
81
|
+
Options for `mcp`:
|
|
82
|
+
|
|
83
|
+
- `--tools <file.json>`: check an exported tool list
|
|
84
|
+
- `--connect`: launch each configured local server and list its tools
|
|
85
|
+
- `--yes`: skip the `--connect` questions; servers that are not pinned to a version are still skipped
|
|
86
|
+
- `--allow-unpinned`: with `--connect --yes`, launch unpinned servers too
|
|
87
|
+
- `--update-lock`: write tool description hashes to `.surfscan/mcp.lock.json`; needs `--tools` or `--connect`
|
|
88
|
+
|
|
89
|
+
### Checking MCP tool descriptions
|
|
90
|
+
|
|
91
|
+
Tool descriptions are not in config files; they come from the running server. MCP-001 and MCP-002 therefore need a tool list, and `surfscan mcp` and `surfscan scan` skip them without one. `scan` cannot take a tool list, so run `surfscan mcp --tools` as well. This skip does not count as incomplete, so `--strict` still passes.
|
|
92
|
+
|
|
93
|
+
A tool list can be a `tools/list` result (`{ "tools": [...] }`, optionally inside a JSON-RPC `result`), a bare array, or `{ "servers": { "<name>": { "tools": [...] } } }`.
|
|
94
|
+
|
|
95
|
+
`--connect` runs third-party code on your machine with your permissions. It never runs by default or under `scan`. What it does to limit the risk:
|
|
96
|
+
|
|
97
|
+
- It shows each server's command and the names of its `env` variables, then asks about that server alone. You can approve some and skip others. It refuses when nobody can answer, unless you pass `--yes`.
|
|
98
|
+
- A server that is not pinned to a version (for example `npx some-server`) runs whatever is newest, so it needs you to type the full word `yes`. With `--yes`, unpinned servers are skipped unless you also pass `--allow-unpinned`.
|
|
99
|
+
- Servers start with a minimal environment: `PATH`, home and temp folders, and proxy and certificate settings. Your other shell variables, such as tokens, are not passed on. The only way to give a server one is a `${VAR}` reference in its `env` block, and the prompt shows those names. `${VAR}` in a command or argument is passed as written, because anyone on the machine can read a command line. On Windows, a server with `%VAR%` in its command or arguments is not launched, because the shell would change what you approved.
|
|
100
|
+
- Remote specs (`github:`, `git+https:`, tarball URLs) count as unpinned unless they name a full commit hash, because what they point at can change.
|
|
101
|
+
- With `--update-lock`, servers that were skipped or failed to start keep their saved hashes.
|
|
102
|
+
|
|
103
|
+
This reduces what a server can reach; it is not a sandbox. A launched server can still read your files and use your network. To check servers you do not trust, run `--connect` in a container or throwaway machine, save the lock, and use `--tools` elsewhere.
|
|
104
|
+
|
|
105
|
+
## Rules
|
|
106
|
+
|
|
107
|
+
Details, examples and detection choices for each rule are in [docs/rules.md](docs/rules.md).
|
|
108
|
+
|
|
109
|
+
| ID | Severity | Finding |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| RULES-001 | high | Invisible Unicode in instruction file |
|
|
112
|
+
| RULES-002 | high | Hidden instructions in comments or markup |
|
|
113
|
+
| RULES-003 | high | Download piped into a shell |
|
|
114
|
+
| RULES-004 | high | Instruction to read or send credentials |
|
|
115
|
+
| RULES-005 | high | Instruction to override earlier instructions or skip confirmation |
|
|
116
|
+
| RULES-006 | medium | Long encoded block |
|
|
117
|
+
| RULES-007 | medium | Instruction to install unpinned packages |
|
|
118
|
+
| RULES-008 | medium | Instruction to fetch and follow an external URL |
|
|
119
|
+
| MCP-001 | high | Injection-style text in tool description |
|
|
120
|
+
| MCP-002 | high | Tool description changed since the saved lock |
|
|
121
|
+
| MCP-003 | high | Server with filesystem access to the home directory or root |
|
|
122
|
+
| MCP-004 | medium | Server launched from an unpinned remote package |
|
|
123
|
+
| MCP-005 | medium | Server reached over plain http or an unknown host |
|
|
124
|
+
| MCP-006 | info | No lock file yet |
|
|
125
|
+
| DEPS-001 | high | Package does not exist on the registry |
|
|
126
|
+
| DEPS-002 | high | Name very close to a popular package |
|
|
127
|
+
| DEPS-003 | medium | Recent maintainer or owner change |
|
|
128
|
+
| DEPS-004 | medium | Newly published package |
|
|
129
|
+
| DEPS-005 | medium | Install scripts present |
|
|
130
|
+
| DEPS-006 | low | Unpinned version |
|
|
131
|
+
| DEPS-007 | low | Deprecated package |
|
|
132
|
+
| KEYS-001 | high | Live or production key |
|
|
133
|
+
| KEYS-002 | high | Env file with secrets not covered by .gitignore |
|
|
134
|
+
| KEYS-003 | medium | Wildcard or admin scope in cloud policy |
|
|
135
|
+
| APPR-001 | high | Bypass or auto-approve mode enabled |
|
|
136
|
+
| APPR-002 | high | Allowlist includes destructive commands |
|
|
137
|
+
| APPR-003 | high | Wildcard shell permission |
|
|
138
|
+
| APPR-004 | medium | All project MCP servers or tools auto-approved |
|
|
139
|
+
| APPR-005 | medium | File edits auto-approved |
|
|
140
|
+
|
|
141
|
+
## Exit codes
|
|
142
|
+
|
|
143
|
+
| Code | Meaning |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| 0 | Nothing at or above `--fail-on` |
|
|
146
|
+
| 1 | At least one finding at or above `--fail-on` |
|
|
147
|
+
| 2 | Usage or runtime error, or with `--strict`, a check that could not run |
|
|
148
|
+
|
|
149
|
+
If the registry cannot be reached, `deps` prints a warning to stderr and reports only what it could check. By default the exit code does not change, so exit 0 means "nothing found among the checks that ran", not "verified". The text report ends with an `Incomplete:` line, and the JSON report has `"complete": false` and a `warnings` array.
|
|
150
|
+
|
|
151
|
+
In CI, pass `--strict`. If any check could not run, surfscan exits 2 instead of 0. Findings still take priority: exit 1 wins over exit 2. `--offline` is a choice, so it does not count as incomplete. These count as a check that could not run:
|
|
152
|
+
|
|
153
|
+
- The registry could not be reached.
|
|
154
|
+
- A config, manifest or lock file could not be parsed.
|
|
155
|
+
- A file was too large to read. The limit is 2 MiB, or 32 MiB for the files `deps` and `keys` read, since lock files and data files are often large. Exclude a generated file with `ignorePaths`.
|
|
156
|
+
- A link was not followed: a link to an agent folder such as `.claude`, or to a file outside the project. A link to a file inside the project is scanned.
|
|
157
|
+
- With `--connect`, a server failed, or `--yes` skipped it for not being pinned.
|
|
158
|
+
- A rule failed.
|
|
159
|
+
|
|
160
|
+
## Config
|
|
161
|
+
|
|
162
|
+
`surfscan.json`:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"ignore": ["RULES-006"],
|
|
167
|
+
"ignorePaths": ["vendor/**"],
|
|
168
|
+
"deps": { "minAgeDays": 30, "trustedPackages": ["my-new-lib"] }
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `ignore`: rule IDs to skip
|
|
173
|
+
- `ignorePaths`: globs to exclude from scanning
|
|
174
|
+
- `deps.minAgeDays`: DEPS-004 threshold in days (default 30)
|
|
175
|
+
- `deps.trustedPackages`: package names the DEPS-004 age check skips (surfscan itself is always skipped). Other DEPS rules still apply. A project's `surfscan.json` is controlled by whoever owns the project, so do not rely on it when scanning code you do not trust.
|
|
176
|
+
|
|
177
|
+
Unknown keys are rejected with an error.
|
|
178
|
+
|
|
179
|
+
When a `surfscan.json` in the scanned directory turns checks off, surfscan says which ones in a warning, on stderr and in the JSON `warnings` array. A file you pass with `--config` is your own choice, so it gets no warning.
|
|
180
|
+
|
|
181
|
+
## What surfscan does not do
|
|
182
|
+
|
|
183
|
+
- It does not check keys against providers. "Live" refers to a key's format.
|
|
184
|
+
- It does not execute project or agent code. The one exception is `mcp --connect`, which is opt-in and asks first unless you pass `--yes`.
|
|
185
|
+
- It does not fix anything. The only file it writes is the MCP lock file, and only with `--update-lock`.
|
|
186
|
+
- It does not cover every agent or file. It reads the files listed in [docs/rules.md](docs/rules.md), so others, such as `GEMINI.md` and `.claude/commands`, are not scanned. `approvals` reads Claude Code settings only, and does not check `hooks` or `apiKeyHelper`. A clean result says nothing about what it does not read.
|
|
187
|
+
- It does not replace code review. Detection is static pattern matching, with the false positives and false negatives described in [docs/rules.md](docs/rules.md).
|
|
188
|
+
|
|
189
|
+
Only `deps`, and `scan` because it runs `deps`, uses the network. They contact npm's registry and download count endpoints and send package names only.
|