coverme-scanner 1.2.0 → 1.3.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.
@@ -0,0 +1,353 @@
1
+ # CoverMe Runtime Verification Agent
2
+
3
+ Compare your **actual runtime environment** against **code configuration** to find dangerous mismatches.
4
+
5
+ ## Arguments
6
+
7
+ $ARGUMENTS - Environment name (from `coverme verify list`)
8
+
9
+ ---
10
+
11
+ ## CRITICAL: This agent requires SSH access
12
+
13
+ Before running, ensure:
14
+ 1. SSH environment is configured: `coverme verify setup --host user@server --name production`
15
+ 2. You have SSH key or password ready
16
+ 3. The environment name is passed as argument
17
+
18
+ ---
19
+
20
+ ## PHASE 1: Load Environment Configuration
21
+
22
+ ```bash
23
+ cat .coverme/runtime.json 2>/dev/null || echo "NO_RUNTIME_CONFIG"
24
+ ```
25
+
26
+ If no config found, output:
27
+ ```
28
+ ERROR: No runtime environments configured.
29
+
30
+ To set up runtime verification:
31
+ coverme verify setup --host user@server.com --name production
32
+
33
+ Then run:
34
+ /coverme-verify production
35
+ ```
36
+
37
+ Extract the environment details:
38
+ - `host`: SSH host (user@server)
39
+ - `port`: SSH port
40
+ - `keyPath`: Optional SSH key path
41
+
42
+ ---
43
+
44
+ ## PHASE 2: Gather Code Expectations
45
+
46
+ Before SSH, analyze the codebase to understand EXPECTED runtime configuration:
47
+
48
+ ### Check Dockerfile/Docker Compose
49
+ ```bash
50
+ cat Dockerfile 2>/dev/null
51
+ cat docker-compose*.yml 2>/dev/null
52
+ ```
53
+
54
+ Look for:
55
+ - `USER` directive (what user should container run as?)
56
+ - `EXPOSE` ports
57
+ - Environment variables
58
+ - Volume mounts
59
+
60
+ ### Check Kubernetes/Helm
61
+ ```bash
62
+ cat k8s/*.yaml helm/**/values.yaml 2>/dev/null
63
+ ```
64
+
65
+ Look for:
66
+ - `securityContext.runAsUser`
67
+ - `securityContext.runAsNonRoot`
68
+ - `securityContext.readOnlyRootFilesystem`
69
+ - Resource limits
70
+ - Service accounts
71
+
72
+ ### Check PM2/Process Manager
73
+ ```bash
74
+ cat ecosystem.config.js pm2.config.js 2>/dev/null
75
+ ```
76
+
77
+ Look for:
78
+ - `user` field
79
+ - `cwd` working directory
80
+ - Environment variables
81
+
82
+ ### Check Systemd
83
+ ```bash
84
+ cat *.service 2>/dev/null
85
+ ```
86
+
87
+ Look for:
88
+ - `User=` directive
89
+ - `Group=` directive
90
+ - `WorkingDirectory=`
91
+
92
+ ### Build Expected State Object
93
+ ```json
94
+ {
95
+ "expected": {
96
+ "user": "appuser",
97
+ "uid": 1000,
98
+ "workDir": "/app",
99
+ "ports": [3000, 8080],
100
+ "env": ["NODE_ENV=production"],
101
+ "readOnlyFs": true,
102
+ "source": "Dockerfile line 15: USER appuser"
103
+ }
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## PHASE 3: SSH and Gather Runtime State
110
+
111
+ **IMPORTANT**: Use the Bash tool with SSH commands. The user has pre-configured SSH access.
112
+
113
+ Build SSH command prefix:
114
+ ```
115
+ SSH_CMD="ssh -o StrictHostKeyChecking=no -p {port} {keyPath ? '-i ' + keyPath : ''} {host}"
116
+ ```
117
+
118
+ ### Check Running Processes
119
+
120
+ ```bash
121
+ # Find Node.js/Python/Java processes
122
+ $SSH_CMD "ps aux | grep -E 'node|python|java|pm2' | grep -v grep"
123
+
124
+ # Get current user
125
+ $SSH_CMD "whoami"
126
+
127
+ # Get user running the app
128
+ $SSH_CMD "ps -eo user,pid,cmd | grep -E 'node|python|java' | head -5"
129
+ ```
130
+
131
+ ### Check Docker Containers (if Docker)
132
+
133
+ ```bash
134
+ # List running containers
135
+ $SSH_CMD "docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}'"
136
+
137
+ # Get user inside container
138
+ $SSH_CMD "docker exec {container_name} whoami"
139
+
140
+ # Get container user ID
141
+ $SSH_CMD "docker exec {container_name} id"
142
+
143
+ # Check container security
144
+ $SSH_CMD "docker inspect {container_name} --format '{{.Config.User}} {{.HostConfig.ReadonlyRootfs}} {{.HostConfig.Privileged}}'"
145
+ ```
146
+
147
+ ### Check Kubernetes Pods (if K8s)
148
+
149
+ ```bash
150
+ # Get pods
151
+ $SSH_CMD "kubectl get pods -o wide"
152
+
153
+ # Get security context
154
+ $SSH_CMD "kubectl get pod {pod_name} -o jsonpath='{.spec.securityContext}'"
155
+
156
+ # Get container user
157
+ $SSH_CMD "kubectl exec {pod_name} -- id"
158
+ ```
159
+
160
+ ### Check PM2 Processes
161
+
162
+ ```bash
163
+ # PM2 status
164
+ $SSH_CMD "pm2 list"
165
+
166
+ # PM2 process details
167
+ $SSH_CMD "pm2 show 0 | grep -E 'user|uid|gid|cwd'"
168
+ ```
169
+
170
+ ### Check System Permissions
171
+
172
+ ```bash
173
+ # Check file permissions on app directory
174
+ $SSH_CMD "ls -la /app/ 2>/dev/null || ls -la /home/*/app/ 2>/dev/null"
175
+
176
+ # Check who owns the app files
177
+ $SSH_CMD "stat -c '%U:%G %a %n' /app/* 2>/dev/null | head -10"
178
+
179
+ # Check sensitive files
180
+ $SSH_CMD "ls -la /app/.env* /app/config/*.json 2>/dev/null"
181
+ ```
182
+
183
+ ### Check Network
184
+
185
+ ```bash
186
+ # Check listening ports
187
+ $SSH_CMD "netstat -tlnp 2>/dev/null || ss -tlnp"
188
+
189
+ # Check firewall rules
190
+ $SSH_CMD "iptables -L -n 2>/dev/null || ufw status 2>/dev/null"
191
+ ```
192
+
193
+ ### Build Actual State Object
194
+ ```json
195
+ {
196
+ "actual": {
197
+ "user": "root",
198
+ "uid": 0,
199
+ "workDir": "/app",
200
+ "ports": [3000, 8080, 6379],
201
+ "env": ["NODE_ENV=production"],
202
+ "readOnlyFs": false,
203
+ "privileged": false,
204
+ "containerUser": "root",
205
+ "processUser": "root"
206
+ }
207
+ }
208
+ ```
209
+
210
+ ---
211
+
212
+ ## PHASE 4: Compare and Generate Findings
213
+
214
+ Compare expected vs actual for EACH field:
215
+
216
+ ### Critical Mismatches (RUNTIME prefix)
217
+
218
+ | Finding | Severity | When to Report |
219
+ |---------|----------|----------------|
220
+ | RUNTIME-001 | CRITICAL | Code says `USER appuser`, runtime runs as `root` |
221
+ | RUNTIME-002 | CRITICAL | Dockerfile has `USER`, but container runs as root |
222
+ | RUNTIME-003 | HIGH | `runAsNonRoot: true` in K8s, but pod runs as root |
223
+ | RUNTIME-004 | HIGH | ReadOnlyRootFilesystem expected, but writable |
224
+ | RUNTIME-005 | HIGH | Privileged container not expected |
225
+ | RUNTIME-006 | MEDIUM | Port exposed that shouldn't be (e.g., Redis 6379) |
226
+ | RUNTIME-007 | MEDIUM | Environment variables missing or different |
227
+ | RUNTIME-008 | MEDIUM | File permissions too open (777, world-writable) |
228
+ | RUNTIME-009 | LOW | Working directory mismatch |
229
+ | RUNTIME-010 | INFO | Extra processes running not in config |
230
+
231
+ ### Output Format
232
+
233
+ ```json
234
+ {
235
+ "id": "RUNTIME-001",
236
+ "title": "Container running as root despite USER directive",
237
+ "severity": "critical",
238
+ "category": "runtime-mismatch",
239
+ "fixOwner": "devops",
240
+ "fixType": "infrastructure",
241
+
242
+ "expected": {
243
+ "value": "appuser (uid 1000)",
244
+ "source": "Dockerfile:15 - USER appuser",
245
+ "code": "USER appuser"
246
+ },
247
+
248
+ "actual": {
249
+ "value": "root (uid 0)",
250
+ "source": "docker exec container_name id",
251
+ "evidence": "uid=0(root) gid=0(root) groups=0(root)"
252
+ },
253
+
254
+ "description": "The Dockerfile specifies USER appuser, but the container is actually running as root. This happened because the docker run command included --user root or the orchestration layer overrode the USER directive.",
255
+
256
+ "impact": "Running as root inside the container means any code execution vulnerability (like the DuckDB file read issue) can access ALL files on the system, not just application files. Attackers can read /etc/shadow, modify system configs, and potentially escape the container.",
257
+
258
+ "recommendation": "1. Check docker-compose.yml for user: root override\n2. Check K8s deployment for securityContext.runAsUser: 0\n3. Ensure no --user root in docker run commands\n4. Add securityContext.runAsNonRoot: true to K8s",
259
+
260
+ "commands": {
261
+ "verify": "docker exec container_name id",
262
+ "fix_docker": "docker run --user 1000:1000 ...",
263
+ "fix_k8s": "kubectl patch deployment app -p '{\"spec\":{\"template\":{\"spec\":{\"securityContext\":{\"runAsNonRoot\":true}}}}}'"
264
+ }
265
+ }
266
+ ```
267
+
268
+ ---
269
+
270
+ ## PHASE 5: Generate Verification Report
271
+
272
+ Save to `.coverme/runtime-verify.json`:
273
+
274
+ ```json
275
+ {
276
+ "environment": "production",
277
+ "host": "user@server.com",
278
+ "verifiedAt": "2024-01-15T10:30:00Z",
279
+
280
+ "summary": {
281
+ "total": 5,
282
+ "critical": 1,
283
+ "high": 2,
284
+ "medium": 1,
285
+ "low": 1,
286
+ "matches": 8
287
+ },
288
+
289
+ "mismatches": [
290
+ { "...finding objects..." }
291
+ ],
292
+
293
+ "matches": [
294
+ {
295
+ "check": "NODE_ENV",
296
+ "expected": "production",
297
+ "actual": "production",
298
+ "status": "match"
299
+ }
300
+ ],
301
+
302
+ "rawData": {
303
+ "expected": { "...from code analysis..." },
304
+ "actual": { "...from SSH commands..." }
305
+ }
306
+ }
307
+ ```
308
+
309
+ ---
310
+
311
+ ## PHASE 6: Display Results
312
+
313
+ Print summary:
314
+
315
+ ```
316
+ Runtime Verification: production (user@server.com)
317
+ =================================================
318
+
319
+ CRITICAL MISMATCH: Container running as root!
320
+
321
+ Expected: USER appuser (from Dockerfile:15)
322
+ Actual: root (uid=0)
323
+
324
+ This is why your DuckDB file read vulnerability was exploitable!
325
+ The code assumed it would run as appuser with limited permissions,
326
+ but in production it runs as root with access to everything.
327
+
328
+ Fix: Add to your deployment:
329
+ securityContext:
330
+ runAsNonRoot: true
331
+ runAsUser: 1000
332
+
333
+ --------------------------------------------------
334
+
335
+ Summary:
336
+ Matches: 8 configurations match
337
+ Mismatches: 5 found
338
+ - 1 CRITICAL (user mismatch)
339
+ - 2 HIGH (permissions)
340
+ - 1 MEDIUM (ports)
341
+ - 1 LOW (working directory)
342
+
343
+ Full report: .coverme/runtime-verify.json
344
+ ```
345
+
346
+ ---
347
+
348
+ ## Security Notes
349
+
350
+ - SSH credentials are NOT stored by CoverMe
351
+ - Commands are read-only (no modifications to remote system)
352
+ - All SSH output is included in the report for audit
353
+ - Consider using a dedicated read-only SSH user for verification
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coverme-scanner",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "AI-powered code scanner with multi-agent verification for Claude Code. One command scans everything.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -121,4 +121,131 @@ agentCmd
121
121
  console.log(`Removed agent "${removed.name}"`);
122
122
  });
123
123
 
124
+ // Runtime verification commands
125
+ const verifyCmd = program
126
+ .command('verify')
127
+ .description('Verify runtime environment matches code expectations');
128
+
129
+ verifyCmd
130
+ .command('setup')
131
+ .description('Configure SSH access for runtime verification')
132
+ .option('-h, --host <host>', 'SSH host (e.g., user@server.com)')
133
+ .option('-p, --port <port>', 'SSH port', '22')
134
+ .option('-k, --key <path>', 'Path to SSH private key')
135
+ .option('-n, --name <name>', 'Environment name (e.g., production, staging)')
136
+ .action((options: { host?: string; port?: string; key?: string; name?: string }) => {
137
+ const covermeDir = join(process.cwd(), '.coverme');
138
+ const configPath = join(covermeDir, 'runtime.json');
139
+
140
+ if (!existsSync(covermeDir)) {
141
+ mkdirSync(covermeDir, { recursive: true });
142
+ }
143
+
144
+ let config: any = { environments: [] };
145
+ if (existsSync(configPath)) {
146
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
147
+ if (!config.environments) config.environments = [];
148
+ }
149
+
150
+ if (!options.host) {
151
+ console.log('\nRuntime Verification Setup');
152
+ console.log('==========================\n');
153
+ console.log('This feature allows CoverMe to SSH into your servers and compare');
154
+ console.log('the actual runtime environment against your code configuration.\n');
155
+ console.log('Usage:');
156
+ console.log(' coverme verify setup --host user@server.com --name production');
157
+ console.log(' coverme verify setup --host deploy@staging.example.com --key ~/.ssh/id_rsa --name staging\n');
158
+ console.log('Options:');
159
+ console.log(' -h, --host <host> SSH host (required)');
160
+ console.log(' -n, --name <name> Environment name (default: from host)');
161
+ console.log(' -p, --port <port> SSH port (default: 22)');
162
+ console.log(' -k, --key <path> Path to SSH private key\n');
163
+
164
+ if (config.environments.length > 0) {
165
+ console.log('Configured environments:');
166
+ config.environments.forEach((env: any, i: number) => {
167
+ console.log(` ${i + 1}. ${env.name}: ${env.host}:${env.port}`);
168
+ });
169
+ }
170
+ return;
171
+ }
172
+
173
+ const envName = options.name || options.host.split('@')[1]?.split('.')[0] || 'default';
174
+
175
+ // Remove existing with same name
176
+ config.environments = config.environments.filter((e: any) => e.name !== envName);
177
+
178
+ config.environments.push({
179
+ name: envName,
180
+ host: options.host,
181
+ port: parseInt(options.port || '22'),
182
+ keyPath: options.key || null,
183
+ addedAt: new Date().toISOString()
184
+ });
185
+
186
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
187
+ console.log(`\nAdded environment "${envName}"`);
188
+ console.log(` Host: ${options.host}`);
189
+ console.log(` Port: ${options.port || '22'}`);
190
+ if (options.key) console.log(` Key: ${options.key}`);
191
+ console.log('\nRun verification with:');
192
+ console.log(` /coverme-verify ${envName}`);
193
+ console.log('\nOr in Claude Code:');
194
+ console.log(` /coverme --verify ${envName}`);
195
+ });
196
+
197
+ verifyCmd
198
+ .command('list')
199
+ .description('List configured environments')
200
+ .action(() => {
201
+ const configPath = join(process.cwd(), '.coverme', 'runtime.json');
202
+
203
+ if (!existsSync(configPath)) {
204
+ console.log('No environments configured.');
205
+ console.log('Run: coverme verify setup --host user@server.com --name production');
206
+ return;
207
+ }
208
+
209
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
210
+
211
+ if (!config.environments || config.environments.length === 0) {
212
+ console.log('No environments configured.');
213
+ return;
214
+ }
215
+
216
+ console.log('\nConfigured Environments:\n');
217
+ config.environments.forEach((env: any, i: number) => {
218
+ console.log(` ${i + 1}. ${env.name}`);
219
+ console.log(` Host: ${env.host}:${env.port}`);
220
+ if (env.keyPath) console.log(` Key: ${env.keyPath}`);
221
+ console.log(` Added: ${new Date(env.addedAt).toLocaleDateString()}`);
222
+ console.log('');
223
+ });
224
+ });
225
+
226
+ verifyCmd
227
+ .command('remove')
228
+ .description('Remove an environment')
229
+ .argument('<name>', 'Environment name')
230
+ .action((name: string) => {
231
+ const configPath = join(process.cwd(), '.coverme', 'runtime.json');
232
+
233
+ if (!existsSync(configPath)) {
234
+ console.error('No environments configured.');
235
+ return;
236
+ }
237
+
238
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
239
+ const idx = config.environments.findIndex((e: any) => e.name.toLowerCase() === name.toLowerCase());
240
+
241
+ if (idx === -1) {
242
+ console.error(`Environment "${name}" not found`);
243
+ return;
244
+ }
245
+
246
+ const removed = config.environments.splice(idx, 1)[0];
247
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
248
+ console.log(`Removed environment "${removed.name}"`);
249
+ });
250
+
124
251
  program.parse();
package/src/cli/init.ts CHANGED
@@ -609,6 +609,7 @@ export async function init(options: InitOptions): Promise<void> {
609
609
  "Bash(git ls-files:*)",
610
610
  "Bash(git log:*)",
611
611
  "Bash(grep:*)",
612
+ "Bash(ssh:*)",
612
613
  "Read(.coverme/*)",
613
614
  "Write(.coverme/*)",
614
615
  "Edit(.coverme/*)"
@@ -649,7 +650,7 @@ export async function init(options: InitOptions): Promise<void> {
649
650
  Usage:
650
651
  1. Open Claude Code in your project
651
652
  2. Type /coverme and press Enter
652
- 3. Wait for the scan to complete
653
+ 3. Wait for the scan to complete (22 AI agents!)
653
654
  4. Report opens automatically in your browser
654
655
 
655
656
  Reports saved to: .coverme/
@@ -657,13 +658,18 @@ Reports saved to: .coverme/
657
658
  - scan_YYYY-MM-DD_HH-MM-SS.json
658
659
 
659
660
  Custom Agents:
660
- Add your own experts to the scan:
661
-
662
661
  coverme agent add "John" "Check all .env files for exposed secrets"
663
- coverme agent add "Sarah" "Find regex patterns vulnerable to ReDoS"
662
+ coverme agent list
663
+ coverme agent remove "John"
664
+
665
+ Runtime Verification (Optional):
666
+ Compare your actual runtime environment against code configuration.
667
+ Catches issues like "Dockerfile says USER appuser but container runs as root"
668
+
669
+ coverme verify setup --host user@server.com --name production
670
+ coverme verify list
664
671
 
665
- List agents: coverme agent list
666
- Remove agent: coverme agent remove "John"
672
+ Once configured, /coverme will automatically SSH and verify runtime.
667
673
 
668
674
  The .coverme/ folder is automatically added to .gitignore
669
675