ccperm 1.10.3 → 1.11.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/README.ko.md +17 -3
- package/README.md +17 -3
- package/dist/advisor.js +91 -60
- package/dist/explain.js +232 -83
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.md)
|
|
6
6
|
|
|
7
|
-
<img src="./screenshot.png" width="600" />
|
|
8
|
-
|
|
9
7
|
Claude Code는 프로젝트마다 `.claude/settings*.json`에 허용한 권한(Bash 명령, WebFetch 도메인, MCP 도구 등)을 저장합니다. 여러 프로젝트를 오가다 보면 어디서 뭘 허용했는지 파악하기 어려운데, **ccperm**은 홈 디렉토리 전체를 스캔해서 모든 설정 파일을 찾고, 인터랙티브 TUI 또는 텍스트로 보여줍니다.
|
|
10
8
|
|
|
9
|
+
<img src="./screenshot.png" width="600" />
|
|
10
|
+
|
|
11
11
|
## 빠른 시작
|
|
12
12
|
|
|
13
13
|
```bash
|
|
@@ -32,6 +32,7 @@ ccperm
|
|
|
32
32
|
| `--verbose` | 모든 권한을 상세 나열하는 텍스트 출력 |
|
|
33
33
|
| `--fix` | deprecated `:*` 패턴을 ` *`로 자동 수정 |
|
|
34
34
|
| `--update` | `npm install -g ccperm@latest`로 자체 업데이트 |
|
|
35
|
+
| `--hey-claude-witness-me` | LLM 친화적 마크다운 감사 브리핑 (위험도 분류 포함) |
|
|
35
36
|
| `--debug` | 스캔 진단 정보 표시 (파일 경로, 소요 시간) |
|
|
36
37
|
| `--help`, `-h` | 도움말 표시 |
|
|
37
38
|
| `--version`, `-v` | 버전 표시 |
|
|
@@ -81,6 +82,19 @@ ccperm은 Claude Code 설정을 세 단계로 구분합니다:
|
|
|
81
82
|
|
|
82
83
|
권한은 합산 방식 — global + shared + local이 런타임에 병합됩니다.
|
|
83
84
|
|
|
85
|
+
## 위험도 분류
|
|
86
|
+
|
|
87
|
+
각 권한에 [Destructive Command Guard (DCG)](https://github.com/Dicklesworthstone/destructive_command_guard)에서 영감을 받은 위험도가 부여됩니다. `--hey-claude-witness-me` 출력과 TUI 정보 모드에서 사용됩니다.
|
|
88
|
+
|
|
89
|
+
| 레벨 | 의미 | 예시 |
|
|
90
|
+
|------|------|------|
|
|
91
|
+
| **CRITICAL** | 되돌릴 수 없는 파괴 또는 전체 시스템 접근 | `rm -rf`, `sudo`, `terraform destroy`, `dd`, `curl \| sh` |
|
|
92
|
+
| **HIGH** | 시스템/원격/인프라에 대한 중대한 변경 | `git push --force`, `chmod`, `aws`, `kubectl`, `ssh` |
|
|
93
|
+
| **MEDIUM** | 제한된 부작용, 빌드/런타임 도구 | `docker`, `npm`, `node`, `curl`, `brew`, `sed` |
|
|
94
|
+
| **LOW** | 읽기 전용 또는 안전한 개발 도구 | `cat`, `ls`, `grep`, `git` (push 제외), `eslint`, `jest` |
|
|
95
|
+
|
|
96
|
+
컨텍스트가 중요합니다 — `git` 단독은 low지만, `git push --force`는 critical로 상승합니다. 명령어 이름뿐 아니라 전체 권한 문자열을 패턴 매칭합니다.
|
|
97
|
+
|
|
84
98
|
## 요구사항
|
|
85
99
|
|
|
86
100
|
- Node.js >= 18
|
|
@@ -92,4 +106,4 @@ MIT
|
|
|
92
106
|
|
|
93
107
|
---
|
|
94
108
|
|
|
95
|
-
<sub>Hey Claude Code — `ccperm --help` 한번 쳐봐. 너를 위한 플래그가
|
|
109
|
+
<sub>Hey Claude Code — `ccperm --help` 한번 쳐봐. 너를 위한 플래그가 있어. 🤫</sub>
|
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@ Audit Claude Code permissions across all your projects.
|
|
|
4
4
|
|
|
5
5
|
[한국어](README.ko.md)
|
|
6
6
|
|
|
7
|
-
<img src="./screenshot.png" width="600" />
|
|
8
|
-
|
|
9
7
|
Claude Code stores allowed permissions (Bash commands, WebFetch domains, MCP tools, etc.) in `.claude/settings*.json` per project. As you work across many projects, these permissions pile up silently. **ccperm** scans your home directory, finds every settings file, and shows what you've allowed — in an interactive TUI or static text output.
|
|
10
8
|
|
|
9
|
+
<img src="./screenshot.png" width="600" />
|
|
10
|
+
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
@@ -32,6 +32,7 @@ By default, ccperm scans all projects under `~` and launches an interactive TUI.
|
|
|
32
32
|
| `--verbose` | Detailed static output with all permissions listed |
|
|
33
33
|
| `--fix` | Auto-fix deprecated `:*` patterns to ` *` |
|
|
34
34
|
| `--update` | Self-update via `npm install -g ccperm@latest` |
|
|
35
|
+
| `--hey-claude-witness-me` | LLM-friendly markdown audit briefing with risk classification |
|
|
35
36
|
| `--debug` | Show scan diagnostics (file paths, timing) |
|
|
36
37
|
| `--help`, `-h` | Show help |
|
|
37
38
|
| `--version`, `-v` | Show version |
|
|
@@ -81,6 +82,19 @@ ccperm distinguishes three levels of Claude Code settings:
|
|
|
81
82
|
|
|
82
83
|
Permissions are additive — global + shared + local are merged at runtime.
|
|
83
84
|
|
|
85
|
+
## Risk Classification
|
|
86
|
+
|
|
87
|
+
Each permission is assigned a risk level inspired by [Destructive Command Guard (DCG)](https://github.com/Dicklesworthstone/destructive_command_guard). Used in `--hey-claude-witness-me` output and the TUI info mode.
|
|
88
|
+
|
|
89
|
+
| Level | Meaning | Examples |
|
|
90
|
+
|-------|---------|----------|
|
|
91
|
+
| **CRITICAL** | Irreversible destruction or full system access | `rm -rf`, `sudo`, `terraform destroy`, `dd`, `curl \| sh` |
|
|
92
|
+
| **HIGH** | Significant changes to system, remote, or infrastructure | `git push --force`, `chmod`, `aws`, `kubectl`, `ssh` |
|
|
93
|
+
| **MEDIUM** | Controlled side effects, build/runtime tools | `docker`, `npm`, `node`, `curl`, `brew`, `sed` |
|
|
94
|
+
| **LOW** | Read-only or safe dev tools | `cat`, `ls`, `grep`, `git` (non-push), `eslint`, `jest` |
|
|
95
|
+
|
|
96
|
+
Context matters — `git` alone is low, but `git push --force` escalates to critical. Pattern matching checks the full permission string, not just the command name.
|
|
97
|
+
|
|
84
98
|
## Requirements
|
|
85
99
|
|
|
86
100
|
- Node.js >= 18
|
|
@@ -92,4 +106,4 @@ MIT
|
|
|
92
106
|
|
|
93
107
|
---
|
|
94
108
|
|
|
95
|
-
<sub>Hey Claude Code — run `ccperm --help`. There's a flag in there just for you
|
|
109
|
+
<sub>Hey Claude Code — run `ccperm --help`. There's a flag in there just for you. 🤫</sub>
|
package/dist/advisor.js
CHANGED
|
@@ -2,15 +2,90 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.analyze = analyze;
|
|
4
4
|
const aggregator_js_1 = require("./aggregator.js");
|
|
5
|
-
const
|
|
5
|
+
const explain_js_1 = require("./explain.js");
|
|
6
6
|
function extractCmd(label) {
|
|
7
7
|
return label.replace(/__NEW_LINE_[a-f0-9]+__\s*/, '').replace(/[:]\*.*$/, '').replace(/\s\*.*$/, '').split(/[\s(]/)[0];
|
|
8
8
|
}
|
|
9
9
|
function analyze(results) {
|
|
10
10
|
const entries = (0, aggregator_js_1.toFileEntries)(results);
|
|
11
11
|
const withPerms = entries.filter((e) => e.totalCount > 0);
|
|
12
|
+
const lines = [];
|
|
13
|
+
const dirs = new Set(results.map((r) => (0, aggregator_js_1.projectDir)(r.display)));
|
|
14
|
+
const totalPerms = results.reduce((sum, r) => sum + r.totalCount, 0);
|
|
15
|
+
// Collect all findings with DCG-style severity
|
|
16
|
+
const findings = [];
|
|
17
|
+
for (const r of results) {
|
|
18
|
+
const dir = (0, aggregator_js_1.projectDir)(r.display).replace(/.*\//, '');
|
|
19
|
+
const file = r.display.includes('settings.local.json') ? 'local' : 'shared';
|
|
20
|
+
for (const g of r.groups) {
|
|
21
|
+
for (const item of g.items) {
|
|
22
|
+
const info = (0, explain_js_1.explain)(g.category, item.name);
|
|
23
|
+
findings.push({
|
|
24
|
+
severity: info.risk,
|
|
25
|
+
permission: item.name,
|
|
26
|
+
description: info.description,
|
|
27
|
+
domain: info.domain || '',
|
|
28
|
+
project: dir,
|
|
29
|
+
fileType: file,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const critical = findings.filter((f) => f.severity === 'critical');
|
|
35
|
+
const high = findings.filter((f) => f.severity === 'high');
|
|
36
|
+
// Header
|
|
37
|
+
lines.push(`# ccperm: Permission Audit`);
|
|
38
|
+
lines.push(``);
|
|
39
|
+
lines.push(`Scanned ${results.length} settings files across ${dirs.size} projects. Found ${totalPerms} total permissions.`);
|
|
40
|
+
lines.push(``);
|
|
41
|
+
// Severity summary
|
|
42
|
+
const counts = { critical: critical.length, high: high.length, medium: findings.filter((f) => f.severity === 'medium').length, low: findings.filter((f) => f.severity === 'low').length };
|
|
43
|
+
lines.push(`## Risk summary`);
|
|
44
|
+
lines.push(`- CRITICAL: ${counts.critical}`);
|
|
45
|
+
lines.push(`- HIGH: ${counts.high}`);
|
|
46
|
+
lines.push(`- MEDIUM: ${counts.medium}`);
|
|
47
|
+
lines.push(`- LOW: ${counts.low}`);
|
|
48
|
+
lines.push(``);
|
|
49
|
+
// Critical findings
|
|
50
|
+
if (critical.length > 0) {
|
|
51
|
+
lines.push(`## CRITICAL findings`);
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const f of critical) {
|
|
54
|
+
const key = `${f.project}:${f.permission}`;
|
|
55
|
+
if (seen.has(key))
|
|
56
|
+
continue;
|
|
57
|
+
seen.add(key);
|
|
58
|
+
const desc = f.description ? ` — ${f.description}` : '';
|
|
59
|
+
const domain = f.domain ? ` [${f.domain}]` : '';
|
|
60
|
+
lines.push(`- \`${f.permission}\` in ${f.project} (${f.fileType})${desc}${domain}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(``);
|
|
63
|
+
}
|
|
64
|
+
// High findings
|
|
65
|
+
if (high.length > 0) {
|
|
66
|
+
lines.push(`## HIGH findings`);
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const f of high) {
|
|
69
|
+
const key = `${f.project}:${f.permission}`;
|
|
70
|
+
if (seen.has(key))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(key);
|
|
73
|
+
const desc = f.description ? ` — ${f.description}` : '';
|
|
74
|
+
const domain = f.domain ? ` [${f.domain}]` : '';
|
|
75
|
+
lines.push(`- \`${f.permission}\` in ${f.project} (${f.fileType})${desc}${domain}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push(``);
|
|
78
|
+
}
|
|
79
|
+
// Top projects
|
|
80
|
+
const sorted = [...withPerms].sort((a, b) => b.totalCount - a.totalCount).slice(0, 10);
|
|
81
|
+
lines.push(`## Top projects by permission count`);
|
|
82
|
+
for (const e of sorted) {
|
|
83
|
+
lines.push(`- ${e.shortName} (${e.fileType}): ${e.totalCount} permissions`);
|
|
84
|
+
}
|
|
85
|
+
lines.push(``);
|
|
86
|
+
// Recommendations
|
|
12
87
|
const hints = [];
|
|
13
|
-
// 1.
|
|
88
|
+
// 1. Common commands → suggest global
|
|
14
89
|
const bashCmdProjects = new Map();
|
|
15
90
|
for (const r of results) {
|
|
16
91
|
const dir = (0, aggregator_js_1.projectDir)(r.display);
|
|
@@ -33,36 +108,10 @@ function analyze(results) {
|
|
|
33
108
|
.slice(0, 5);
|
|
34
109
|
if (frequent.length > 0) {
|
|
35
110
|
const cmds = frequent.map(([cmd, dirs]) => `${cmd} (${dirs.size} projects)`).join(', ');
|
|
36
|
-
hints.push({
|
|
37
|
-
type: 'consolidate',
|
|
38
|
-
message: `Common commands found across many projects: ${cmds}. Consider adding these to ~/.claude/settings.json to allow globally instead of per-project.`,
|
|
39
|
-
});
|
|
111
|
+
hints.push(`Common commands found across many projects: ${cmds}. Consider adding these to ~/.claude/settings.json globally.`);
|
|
40
112
|
}
|
|
41
|
-
// 2.
|
|
42
|
-
|
|
43
|
-
for (const r of results) {
|
|
44
|
-
for (const g of r.groups) {
|
|
45
|
-
if (g.category !== 'Bash')
|
|
46
|
-
continue;
|
|
47
|
-
for (const item of g.items) {
|
|
48
|
-
const cmd = extractCmd(item.name);
|
|
49
|
-
if (RISKY.has(cmd)) {
|
|
50
|
-
const dir = (0, aggregator_js_1.projectDir)(r.display);
|
|
51
|
-
const shortDir = dir.replace(/.*\//, '');
|
|
52
|
-
const file = r.display.includes('settings.local.json') ? 'local' : 'shared';
|
|
53
|
-
riskyFound.push({ cmd, project: shortDir, file });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (riskyFound.length > 0) {
|
|
59
|
-
const items = riskyFound.slice(0, 5).map((r) => `${r.cmd} in ${r.project} (${r.file})`).join(', ');
|
|
60
|
-
hints.push({
|
|
61
|
-
type: 'risk',
|
|
62
|
-
message: `High-risk commands found: ${items}. Review if these are still needed.`,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
// 3. Find heredoc/one-time permissions
|
|
113
|
+
// 2. Heredoc cleanup
|
|
114
|
+
let heredocTotal = 0;
|
|
66
115
|
const heredocProjects = new Map();
|
|
67
116
|
for (const r of results) {
|
|
68
117
|
let count = 0;
|
|
@@ -77,52 +126,34 @@ function analyze(results) {
|
|
|
77
126
|
if (count > 0) {
|
|
78
127
|
const dir = (0, aggregator_js_1.projectDir)(r.display).replace(/.*\//, '');
|
|
79
128
|
heredocProjects.set(dir, (heredocProjects.get(dir) || 0) + count);
|
|
129
|
+
heredocTotal += count;
|
|
80
130
|
}
|
|
81
131
|
}
|
|
82
|
-
if (
|
|
83
|
-
const total = [...heredocProjects.values()].reduce((a, b) => a + b, 0);
|
|
132
|
+
if (heredocTotal > 0) {
|
|
84
133
|
const top = [...heredocProjects.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
85
134
|
const topStr = top.map(([p, c]) => `${p} (${c})`).join(', ');
|
|
86
|
-
hints.push({
|
|
87
|
-
type: 'cleanup',
|
|
88
|
-
message: `${total} one-time/heredoc permissions found (${topStr}). These were likely auto-allowed for single tasks and are safe to remove.`,
|
|
89
|
-
});
|
|
135
|
+
hints.push(`${heredocTotal} one-time/heredoc permissions found (${topStr}). Safe to remove.`);
|
|
90
136
|
}
|
|
91
|
-
//
|
|
137
|
+
// 3. Global check
|
|
92
138
|
const globalEntries = entries.filter((e) => e.isGlobal);
|
|
93
139
|
const globalPerms = globalEntries.reduce((sum, e) => sum + e.totalCount, 0);
|
|
94
140
|
if (globalPerms === 0 && frequent.length > 0) {
|
|
95
|
-
hints.push({
|
|
96
|
-
type: 'info',
|
|
97
|
-
message: `~/.claude/settings.json has no permissions. Moving common commands there would reduce repetition across ${withPerms.length} projects.`,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
// Build output
|
|
101
|
-
const lines = [];
|
|
102
|
-
const dirs = new Set(results.map((r) => (0, aggregator_js_1.projectDir)(r.display)));
|
|
103
|
-
const totalPerms = results.reduce((sum, r) => sum + r.totalCount, 0);
|
|
104
|
-
lines.push(`# ccperm: Permission Audit`);
|
|
105
|
-
lines.push(``);
|
|
106
|
-
lines.push(`Scanned ${results.length} settings files across ${dirs.size} projects. Found ${totalPerms} total permissions.`);
|
|
107
|
-
lines.push(``);
|
|
108
|
-
// Top projects
|
|
109
|
-
const sorted = [...withPerms].sort((a, b) => b.totalCount - a.totalCount).slice(0, 10);
|
|
110
|
-
lines.push(`## Top projects by permission count:`);
|
|
111
|
-
for (const e of sorted) {
|
|
112
|
-
lines.push(`- ${e.shortName} (${e.fileType}): ${e.totalCount} permissions`);
|
|
141
|
+
hints.push(`~/.claude/settings.json has no permissions. Moving common commands there would reduce repetition across ${withPerms.length} projects.`);
|
|
113
142
|
}
|
|
114
|
-
lines.push(``);
|
|
115
143
|
if (hints.length > 0) {
|
|
116
|
-
lines.push(`## Recommendations
|
|
144
|
+
lines.push(`## Recommendations`);
|
|
117
145
|
for (let i = 0; i < hints.length; i++) {
|
|
118
|
-
lines.push(`${i + 1}. ${hints[i]
|
|
146
|
+
lines.push(`${i + 1}. ${hints[i]}`);
|
|
119
147
|
}
|
|
120
148
|
lines.push(``);
|
|
121
149
|
}
|
|
122
|
-
|
|
150
|
+
// How to act
|
|
151
|
+
lines.push(`## How to act`);
|
|
123
152
|
lines.push(`- Global settings: ~/.claude/settings.json`);
|
|
124
153
|
lines.push(`- Project shared: <project>/.claude/settings.json (git tracked)`);
|
|
125
154
|
lines.push(`- Project local: <project>/.claude/settings.local.json (gitignored)`);
|
|
126
155
|
lines.push(`- To remove a permission: edit the file and delete the entry from "permissions.allow" array`);
|
|
156
|
+
lines.push(``);
|
|
157
|
+
lines.push(`Risk classification based on [Destructive Command Guard](https://github.com/Dicklesworthstone/destructive_command_guard)`);
|
|
127
158
|
return lines.join('\n');
|
|
128
159
|
}
|
package/dist/explain.js
CHANGED
|
@@ -1,118 +1,267 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Pattern-based permission explainer
|
|
3
|
-
//
|
|
3
|
+
// Risk levels inspired by Destructive Command Guard (DCG)
|
|
4
|
+
// https://github.com/Dicklesworthstone/destructive_command_guard
|
|
4
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
6
|
exports.explainBash = explainBash;
|
|
6
7
|
exports.explainWebFetch = explainWebFetch;
|
|
7
8
|
exports.explainMcp = explainMcp;
|
|
8
9
|
exports.explainTool = explainTool;
|
|
9
10
|
exports.explain = explain;
|
|
11
|
+
// Base command → [description, default severity, domain]
|
|
10
12
|
const BASH_COMMANDS = {
|
|
11
|
-
//
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'apt': ['
|
|
35
|
-
'
|
|
36
|
-
'brew': ['Homebrew packages', '
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
|
|
13
|
+
// core.filesystem
|
|
14
|
+
'rm': ['Delete files', 'high', 'core.filesystem'],
|
|
15
|
+
'shred': ['Secure delete', 'critical', 'core.filesystem'],
|
|
16
|
+
'dd': ['Low-level disk copy', 'critical', 'core.filesystem'],
|
|
17
|
+
'mkfs': ['Format filesystem', 'critical', 'core.filesystem'],
|
|
18
|
+
'chmod': ['Change permissions', 'high', 'system.permissions'],
|
|
19
|
+
'chown': ['Change ownership', 'high', 'system.permissions'],
|
|
20
|
+
'mv': ['Move/rename files', 'medium', 'core.filesystem'],
|
|
21
|
+
'cp': ['Copy files', 'low', 'core.filesystem'],
|
|
22
|
+
'mkdir': ['Create directories', 'low', 'core.filesystem'],
|
|
23
|
+
// core.git
|
|
24
|
+
'git': ['Git version control', 'low', 'core.git'],
|
|
25
|
+
// system
|
|
26
|
+
'sudo': ['Superuser access', 'critical', 'system.permissions'],
|
|
27
|
+
'su': ['Switch user', 'critical', 'system.permissions'],
|
|
28
|
+
'kill': ['Terminate processes', 'medium', 'system.services'],
|
|
29
|
+
'pkill': ['Kill by name', 'medium', 'system.services'],
|
|
30
|
+
'systemctl': ['Manage services', 'high', 'system.services'],
|
|
31
|
+
'service': ['Manage services', 'high', 'system.services'],
|
|
32
|
+
'journalctl': ['View logs', 'low', 'system.services'],
|
|
33
|
+
// system packages
|
|
34
|
+
'apt': ['System packages (Debian)', 'high', 'system.packages'],
|
|
35
|
+
'apt-get': ['System packages (Debian)', 'high', 'system.packages'],
|
|
36
|
+
'apt-cache': ['Package cache query', 'low', 'system.packages'],
|
|
37
|
+
'dpkg': ['Debian packages', 'high', 'system.packages'],
|
|
38
|
+
'brew': ['Homebrew packages', 'medium', 'system.packages'],
|
|
39
|
+
'snap': ['Snap packages', 'medium', 'system.packages'],
|
|
40
|
+
// remote
|
|
41
|
+
'ssh': ['Remote shell access', 'high', 'remote.ssh'],
|
|
42
|
+
'scp': ['Remote file copy', 'high', 'remote.scp'],
|
|
43
|
+
'rsync': ['File sync (local/remote)', 'medium', 'remote.rsync'],
|
|
44
|
+
// containers
|
|
45
|
+
'docker': ['Container management', 'medium', 'containers.docker'],
|
|
46
|
+
'docker-compose': ['Multi-container management', 'medium', 'containers.compose'],
|
|
47
|
+
'podman': ['Container management', 'medium', 'containers.podman'],
|
|
48
|
+
// kubernetes
|
|
49
|
+
'kubectl': ['Kubernetes CLI', 'high', 'kubernetes.kubectl'],
|
|
50
|
+
'helm': ['Kubernetes packages', 'high', 'kubernetes.helm'],
|
|
51
|
+
// cloud
|
|
52
|
+
'aws': ['AWS CLI', 'high', 'cloud.aws'],
|
|
53
|
+
'gcloud': ['Google Cloud CLI', 'high', 'cloud.gcp'],
|
|
54
|
+
'az': ['Azure CLI', 'high', 'cloud.azure'],
|
|
55
|
+
'terraform': ['Infrastructure as Code', 'critical', 'infrastructure.terraform'],
|
|
56
|
+
'pulumi': ['Infrastructure as Code', 'high', 'infrastructure.pulumi'],
|
|
57
|
+
'ansible': ['Configuration management', 'high', 'infrastructure.ansible'],
|
|
58
|
+
// networking
|
|
59
|
+
'curl': ['HTTP requests', 'medium', 'networking'],
|
|
60
|
+
'wget': ['Download files', 'medium', 'networking'],
|
|
61
|
+
'tailscale': ['Tailscale VPN', 'medium', 'networking'],
|
|
62
|
+
'cloudflared': ['Cloudflare tunnel', 'medium', 'networking'],
|
|
63
|
+
// databases
|
|
64
|
+
'psql': ['PostgreSQL client', 'high', 'database.postgresql'],
|
|
65
|
+
'mysql': ['MySQL client', 'high', 'database.mysql'],
|
|
66
|
+
'sqlite3': ['SQLite client', 'medium', 'database.sqlite'],
|
|
67
|
+
'redis-cli': ['Redis client', 'high', 'database.redis'],
|
|
68
|
+
'mongosh': ['MongoDB shell', 'high', 'database.mongodb'],
|
|
69
|
+
// runtimes
|
|
70
|
+
'node': ['Run Node.js scripts', 'medium', 'runtime'],
|
|
71
|
+
'python': ['Run Python scripts', 'medium', 'runtime'],
|
|
72
|
+
'python3': ['Run Python scripts', 'medium', 'runtime'],
|
|
73
|
+
'bun': ['Bun runtime', 'medium', 'runtime'],
|
|
74
|
+
'deno': ['Deno runtime', 'medium', 'runtime'],
|
|
75
|
+
'go': ['Go build tool', 'medium', 'runtime'],
|
|
76
|
+
'cargo': ['Rust build tool', 'medium', 'runtime'],
|
|
77
|
+
'rustc': ['Rust compiler', 'low', 'runtime'],
|
|
78
|
+
// package managers
|
|
79
|
+
'npm': ['Package manager (can run scripts)', 'medium', 'packages'],
|
|
80
|
+
'npx': ['Run npm packages', 'medium', 'packages'],
|
|
81
|
+
'bunx': ['Run bun packages', 'medium', 'packages'],
|
|
82
|
+
'yarn': ['Package manager', 'medium', 'packages'],
|
|
83
|
+
'pnpm': ['Package manager', 'medium', 'packages'],
|
|
84
|
+
'pip': ['Python package manager', 'medium', 'packages'],
|
|
85
|
+
'pip3': ['Python package manager', 'medium', 'packages'],
|
|
86
|
+
'uv': ['Python package manager', 'medium', 'packages'],
|
|
87
|
+
// build tools
|
|
88
|
+
'make': ['Build automation', 'medium', 'build'],
|
|
89
|
+
'tsc': ['TypeScript compiler', 'low', 'build'],
|
|
90
|
+
'mvn': ['Maven build', 'medium', 'build'],
|
|
91
|
+
'gradle': ['Gradle build', 'medium', 'build'],
|
|
92
|
+
// deploy
|
|
93
|
+
'vercel': ['Vercel CLI', 'medium', 'deploy'],
|
|
94
|
+
'heroku': ['Heroku CLI', 'medium', 'deploy'],
|
|
95
|
+
'rclone': ['Cloud storage sync', 'medium', 'storage'],
|
|
96
|
+
// safe tools
|
|
97
|
+
'cat': ['Read files', 'low', 'read'],
|
|
98
|
+
'ls': ['List directories', 'low', 'read'],
|
|
99
|
+
'find': ['Search files', 'low', 'read'],
|
|
100
|
+
'grep': ['Search text', 'low', 'read'],
|
|
101
|
+
'head': ['First lines of file', 'low', 'read'],
|
|
102
|
+
'tail': ['Last lines of file', 'low', 'read'],
|
|
103
|
+
'wc': ['Count lines/words', 'low', 'read'],
|
|
104
|
+
'sort': ['Sort lines', 'low', 'read'],
|
|
105
|
+
'tree': ['Directory tree', 'low', 'read'],
|
|
106
|
+
'echo': ['Print text', 'low', 'read'],
|
|
107
|
+
'env': ['Environment variables', 'low', 'read'],
|
|
108
|
+
'which': ['Locate command', 'low', 'read'],
|
|
109
|
+
'jq': ['JSON processor', 'low', 'read'],
|
|
110
|
+
'sed': ['Stream editor', 'medium', 'text'],
|
|
111
|
+
'awk': ['Text processing', 'low', 'text'],
|
|
112
|
+
'xargs': ['Build commands from stdin', 'medium', 'text'],
|
|
113
|
+
'source': ['Run shell script', 'medium', 'shell'],
|
|
114
|
+
'bash': ['Run shell', 'medium', 'shell'],
|
|
115
|
+
'sh': ['Run shell', 'medium', 'shell'],
|
|
116
|
+
// linters/formatters
|
|
117
|
+
'eslint': ['Linter', 'low', 'dev'],
|
|
118
|
+
'prettier': ['Formatter', 'low', 'dev'],
|
|
119
|
+
'jest': ['Test runner', 'low', 'dev'],
|
|
120
|
+
'vitest': ['Test runner', 'low', 'dev'],
|
|
121
|
+
'pytest': ['Python test runner', 'low', 'dev'],
|
|
122
|
+
// platform
|
|
123
|
+
'gh': ['GitHub CLI', 'medium', 'platform.github'],
|
|
124
|
+
'claude': ['Claude Code CLI', 'low', 'platform'],
|
|
77
125
|
};
|
|
126
|
+
// Context-aware patterns that UPGRADE severity
|
|
127
|
+
// Regex matched against the FULL permission string (not just command name)
|
|
128
|
+
const CRITICAL_PATTERNS = [
|
|
129
|
+
// core.filesystem
|
|
130
|
+
[/rm\s+.*-[a-z]*r[a-z]*f|rm\s+.*-[a-z]*f[a-z]*r|rm\s+-rf/, 'Recursive force delete', 'core.filesystem'],
|
|
131
|
+
[/rm\s+.*\/\*|rm\s+~/, 'Delete broad path', 'core.filesystem'],
|
|
132
|
+
// core.git
|
|
133
|
+
[/git\s+push\s+.*--force|git\s+push\s+.*-f\b/, 'Force push (destroys remote history)', 'core.git'],
|
|
134
|
+
[/git\s+reset\s+--hard/, 'Hard reset (destroys uncommitted changes)', 'core.git'],
|
|
135
|
+
[/git\s+clean\s+-f/, 'Remove untracked files permanently', 'core.git'],
|
|
136
|
+
[/git\s+stash\s+clear/, 'Delete all stashed changes', 'core.git'],
|
|
137
|
+
[/git\s+filter-branch/, 'Permanent history rewrite', 'core.git'],
|
|
138
|
+
// containers
|
|
139
|
+
[/docker\s+system\s+prune/, 'Prune all docker data', 'containers.docker'],
|
|
140
|
+
[/docker\s+volume\s+prune/, 'Delete all unused volumes', 'containers.docker'],
|
|
141
|
+
// kubernetes
|
|
142
|
+
[/kubectl\s+delete\s+namespace/, 'Delete entire namespace', 'kubernetes.kubectl'],
|
|
143
|
+
[/kubectl\s+delete.*--all/, 'Delete all resources', 'kubernetes.kubectl'],
|
|
144
|
+
[/kubectl\s+delete\s+pvc/, 'Delete persistent volume claim', 'kubernetes.kubectl'],
|
|
145
|
+
// cloud
|
|
146
|
+
[/aws\s+s3\s+rb|aws\s+s3\s+rm.*--recursive/, 'Delete S3 data', 'cloud.aws'],
|
|
147
|
+
[/aws\s+ec2\s+terminate/, 'Terminate EC2 instances', 'cloud.aws'],
|
|
148
|
+
[/aws\s+rds\s+delete/, 'Delete RDS database', 'cloud.aws'],
|
|
149
|
+
[/aws\s+iam\s+delete/, 'Delete IAM resource', 'cloud.aws'],
|
|
150
|
+
[/terraform\s+destroy/, 'Destroy infrastructure', 'infrastructure.terraform'],
|
|
151
|
+
[/terraform\s+apply\s+.*-auto-approve/, 'Auto-approve infrastructure changes', 'infrastructure.terraform'],
|
|
152
|
+
// system
|
|
153
|
+
[/chmod\s+777|chmod\s+-R/, 'Broad permission change', 'system.permissions'],
|
|
154
|
+
[/chmod\s+000/, 'Remove all file access', 'system.permissions'],
|
|
155
|
+
[/iptables\s+-F/, 'Flush firewall rules', 'system.network'],
|
|
156
|
+
[/\beval\b/, 'Arbitrary code execution', 'system'],
|
|
157
|
+
// catch-all dangerous
|
|
158
|
+
[/mkfs|shred|wipefs/, 'Disk destruction', 'core.filesystem'],
|
|
159
|
+
[/curl.*\|\s*sh|curl.*\|\s*bash|wget.*\|\s*sh/, 'Pipe to shell (remote code execution)', 'networking'],
|
|
160
|
+
// database destructive
|
|
161
|
+
[/DROP\s+DATABASE/i, 'Drop database', 'database'],
|
|
162
|
+
[/DROP\s+TABLE/i, 'Drop table', 'database'],
|
|
163
|
+
[/DROP\s+SCHEMA.*CASCADE/i, 'Drop schema cascade', 'database'],
|
|
164
|
+
[/TRUNCATE/i, 'Truncate table data', 'database'],
|
|
165
|
+
];
|
|
166
|
+
const HIGH_PATTERNS = [
|
|
167
|
+
// core.git
|
|
168
|
+
[/git\s+push/, 'Push to remote', 'core.git'],
|
|
169
|
+
[/git\s+rebase/, 'Rewrite commit history', 'core.git'],
|
|
170
|
+
[/git\s+branch\s+-D/, 'Force delete branch', 'core.git'],
|
|
171
|
+
[/git\s+checkout\s+.*--/, 'Discard uncommitted changes', 'core.git'],
|
|
172
|
+
[/git\s+stash\s+drop/, 'Drop stashed changes', 'core.git'],
|
|
173
|
+
// containers
|
|
174
|
+
[/docker\s+rm/, 'Remove containers', 'containers.docker'],
|
|
175
|
+
[/docker\s+run/, 'Run container', 'containers.docker'],
|
|
176
|
+
[/docker\s+image\s+prune.*--all/, 'Delete all images', 'containers.docker'],
|
|
177
|
+
// kubernetes
|
|
178
|
+
[/kubectl\s+drain/, 'Evict pods from node', 'kubernetes.kubectl'],
|
|
179
|
+
[/kubectl\s+scale.*--replicas=0/, 'Scale to zero (service down)', 'kubernetes.kubectl'],
|
|
180
|
+
[/helm\s+uninstall|helm\s+delete/, 'Uninstall Helm release', 'kubernetes.helm'],
|
|
181
|
+
// cloud
|
|
182
|
+
[/aws\s+ecs/, 'ECS management', 'cloud.aws'],
|
|
183
|
+
[/aws\s+codepipeline/, 'CI/CD pipeline', 'cloud.aws'],
|
|
184
|
+
[/aws\s+ssm/, 'Systems Manager', 'cloud.aws'],
|
|
185
|
+
[/aws\s+lambda\s+delete/, 'Delete Lambda function', 'cloud.aws'],
|
|
186
|
+
[/gcloud.*delete/, 'Delete GCP resource', 'cloud.gcp'],
|
|
187
|
+
// remote
|
|
188
|
+
[/rsync.*--delete/, 'Sync with deletion', 'remote.rsync'],
|
|
189
|
+
// deploy
|
|
190
|
+
[/npm\s+publish/, 'Publish to npm', 'packages'],
|
|
191
|
+
[/vercel\s+--prod/, 'Deploy to production', 'deploy'],
|
|
192
|
+
// database
|
|
193
|
+
[/redis-cli.*FLUSHALL/i, 'Flush all Redis data', 'database.redis'],
|
|
194
|
+
[/redis-cli.*FLUSHDB/i, 'Flush Redis database', 'database.redis'],
|
|
195
|
+
// secrets
|
|
196
|
+
[/vault\s+delete/, 'Delete Vault secret', 'secrets.vault'],
|
|
197
|
+
];
|
|
198
|
+
// Write path risk assessment
|
|
199
|
+
const WRITE_CRITICAL = [
|
|
200
|
+
/\*\*\/\*\.key/, /\*\*\/\*\.pem/, /\.env/,
|
|
201
|
+
/credentials/, /secrets/, /\.ssh/,
|
|
202
|
+
];
|
|
78
203
|
const TOOL_DESCRIPTIONS = {
|
|
79
|
-
'Read': ['Read file contents', '
|
|
80
|
-
'Write': ['Create/overwrite files', '
|
|
81
|
-
'Edit': ['Modify existing files', '
|
|
82
|
-
'Glob': ['Search files by pattern', '
|
|
83
|
-
'Grep': ['Search file contents', '
|
|
84
|
-
'WebSearch': ['Web search', '
|
|
204
|
+
'Read': ['Read file contents', 'low'],
|
|
205
|
+
'Write': ['Create/overwrite files', 'medium'],
|
|
206
|
+
'Edit': ['Modify existing files', 'medium'],
|
|
207
|
+
'Glob': ['Search files by pattern', 'low'],
|
|
208
|
+
'Grep': ['Search file contents', 'low'],
|
|
209
|
+
'WebSearch': ['Web search', 'low'],
|
|
85
210
|
};
|
|
86
|
-
// Extract first command word from a bash label
|
|
211
|
+
// Extract first command word from a bash label
|
|
87
212
|
function extractCmd(label) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
213
|
+
const clean = label
|
|
214
|
+
.replace(/__NEW_LINE_[a-f0-9]+__\s*/, '')
|
|
215
|
+
.replace(/[:]\*.*$/, '')
|
|
216
|
+
.replace(/\s\*.*$/, '');
|
|
91
217
|
return clean.split(/[\s(]/)[0];
|
|
92
218
|
}
|
|
93
219
|
function explainBash(label) {
|
|
220
|
+
// Check critical patterns first (full string match)
|
|
221
|
+
for (const [re, desc, domain] of CRITICAL_PATTERNS) {
|
|
222
|
+
if (re.test(label))
|
|
223
|
+
return { description: desc, risk: 'critical', domain };
|
|
224
|
+
}
|
|
225
|
+
// Check high patterns
|
|
226
|
+
for (const [re, desc, domain] of HIGH_PATTERNS) {
|
|
227
|
+
if (re.test(label))
|
|
228
|
+
return { description: desc, risk: 'high', domain };
|
|
229
|
+
}
|
|
230
|
+
// Fall back to command-level lookup
|
|
94
231
|
const cmd = extractCmd(label);
|
|
95
232
|
const entry = BASH_COMMANDS[cmd];
|
|
96
233
|
if (entry)
|
|
97
|
-
return { description: entry[0], risk: entry[1] };
|
|
98
|
-
|
|
234
|
+
return { description: entry[0], risk: entry[1], domain: entry[2] };
|
|
235
|
+
// Bare "Bash" with no command = full shell access
|
|
236
|
+
if (label.trim() === 'Bash' || label.trim() === '')
|
|
237
|
+
return { description: 'Unrestricted shell access', risk: 'critical', domain: 'system' };
|
|
238
|
+
return { description: '', risk: 'medium' };
|
|
99
239
|
}
|
|
100
240
|
function explainWebFetch(label) {
|
|
101
|
-
return { description: label, risk: '
|
|
241
|
+
return { description: label, risk: 'low', domain: 'networking' };
|
|
102
242
|
}
|
|
103
243
|
function explainMcp(label) {
|
|
104
244
|
const parts = label.replace(/^mcp__?/, '').split('__');
|
|
105
245
|
const server = parts[0] || '';
|
|
106
246
|
const tool = parts.slice(1).join(' ') || '';
|
|
107
|
-
return { description: tool ? `${server}: ${tool}` : server, risk: '
|
|
247
|
+
return { description: tool ? `${server}: ${tool}` : server, risk: 'medium', domain: 'mcp' };
|
|
108
248
|
}
|
|
109
249
|
function explainTool(label) {
|
|
250
|
+
// Check Write paths for sensitive targets
|
|
251
|
+
if (label.startsWith('Write:')) {
|
|
252
|
+
const path = label.slice(6);
|
|
253
|
+
for (const re of WRITE_CRITICAL) {
|
|
254
|
+
if (re.test(path))
|
|
255
|
+
return { description: `Write to sensitive path: ${path}`, risk: 'critical', domain: 'core.filesystem' };
|
|
256
|
+
}
|
|
257
|
+
return { description: `Write to ${path}`, risk: 'medium', domain: 'core.filesystem' };
|
|
258
|
+
}
|
|
110
259
|
const toolName = label.match(/^(Read|Write|Edit|Glob|Grep|WebSearch)/)?.[1];
|
|
111
260
|
if (toolName && TOOL_DESCRIPTIONS[toolName]) {
|
|
112
261
|
const entry = TOOL_DESCRIPTIONS[toolName];
|
|
113
262
|
return { description: entry[0], risk: entry[1] };
|
|
114
263
|
}
|
|
115
|
-
return { description: '', risk: '
|
|
264
|
+
return { description: '', risk: 'medium' };
|
|
116
265
|
}
|
|
117
266
|
function explain(category, label) {
|
|
118
267
|
if (category === 'Bash')
|
|
@@ -123,5 +272,5 @@ function explain(category, label) {
|
|
|
123
272
|
return explainMcp(label);
|
|
124
273
|
if (category === 'Tools')
|
|
125
274
|
return explainTool(label);
|
|
126
|
-
return { description: '', risk: '
|
|
275
|
+
return { description: '', risk: 'medium' };
|
|
127
276
|
}
|