ccperm 1.15.1 → 1.17.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.
- package/README.ko.md +33 -9
- package/README.md +33 -9
- package/dist/advisor.js +27 -1
- package/dist/aggregator.js +5 -1
- package/dist/interactive.js +86 -2
- package/dist/renderer.js +37 -1
- package/dist/scanner.js +19 -4
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -44,15 +44,15 @@ TTY 환경(기본)에서는 박스 프레임 TUI가 실행됩니다:
|
|
|
44
44
|
**목록 뷰** — 프로젝트가 권한 수 기준으로 정렬됩니다. 컬럼: Bash, MCP, Tools, TOTAL, `!` (위험도), `†` (deprecated `:*`), `G` (글로벌과 중복).
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
┌ ccperm
|
|
48
|
-
│ PROJECT
|
|
49
|
-
|
|
50
|
-
│ ~/.claude
|
|
51
|
-
|
|
52
|
-
│ ▸ my-project local
|
|
53
|
-
│ other-app shared
|
|
54
|
-
│
|
|
55
|
-
|
|
47
|
+
┌ ccperm ──────────────────────────────────────────── 1/8 ┐
|
|
48
|
+
│ PROJECT Bash MCP Tools TOTAL │
|
|
49
|
+
├─────────────────────────────────────────────────────────┤
|
|
50
|
+
│ ~/.claude 15 · 2 17 │
|
|
51
|
+
├─────────────────────────────────────────────────────────┤
|
|
52
|
+
│ ▸ my-project local 5 · 3 8 │
|
|
53
|
+
│ other-app shared 2 3 · 5 │
|
|
54
|
+
│ ! risk † deprecated G in global │
|
|
55
|
+
└──── [↑↓] navigate [Enter] detail [/] search [q] quit ┘
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
**상세 뷰** — Enter로 프로젝트를 펼칩니다. 카테고리를 Enter로 접고 펼 수 있습니다.
|
|
@@ -86,6 +86,30 @@ ccperm은 Claude Code 설정을 세 단계로 구분합니다:
|
|
|
86
86
|
|
|
87
87
|
권한은 합산 방식 — global + shared + local이 런타임에 병합됩니다.
|
|
88
88
|
|
|
89
|
+
## Allow vs Deny vs Ask
|
|
90
|
+
|
|
91
|
+
Claude Code 설정은 `permissions.allow`, `permissions.deny`, `permissions.ask`를 모두 지원합니다. ccperm은 이를 명확히 분리합니다:
|
|
92
|
+
|
|
93
|
+
- **Allow** 권한은 메인 목록에 표시되며, 삭제(`[d]`)나 글로벌 복사(`[g]`)가 가능합니다
|
|
94
|
+
- **Deny** 룰은 상세 뷰에서 접이식 **Deny** 섹션에 `DENY` 태그와 함께 표시됩니다
|
|
95
|
+
- **Ask** 룰은 접이식 **Ask** 섹션에 표시됩니다 — 매번 확인을 요청하는 권한입니다
|
|
96
|
+
- Deny와 Ask 룰은 **삭제/복사 불가** — 의도적으로 설정한 제어 규칙입니다
|
|
97
|
+
- `--verbose` 출력에서 프로젝트별 Deny, Ask 섹션이 분리 표시됩니다
|
|
98
|
+
- `--hey-claude-witness-me`에서 deny 룰이 "Protected rules"로 나열됩니다
|
|
99
|
+
- Allow 카운트에 deny, ask 룰은 포함되지 않습니다
|
|
100
|
+
|
|
101
|
+
## 추가 설정
|
|
102
|
+
|
|
103
|
+
ccperm은 다음 최상위 설정도 스캔하여 표시합니다:
|
|
104
|
+
|
|
105
|
+
| 필드 | 설명 |
|
|
106
|
+
|------|------|
|
|
107
|
+
| `allowedTools` | 사용이 허용된 도구 (예: `Bash`, `Read`, `Write`) |
|
|
108
|
+
| `deniedTools` | 사용이 차단된 도구 (예: `WebSearch`) |
|
|
109
|
+
| `additionalDirectories` | 프로젝트 루트 외 Claude Code가 접근할 수 있는 추가 디렉토리 |
|
|
110
|
+
|
|
111
|
+
TUI 상세 뷰에서 접이식 섹션으로, `--verbose` 출력에서 별도 섹션으로 표시됩니다. Deny 룰과 마찬가지로 삭제/복사가 불가합니다.
|
|
112
|
+
|
|
89
113
|
## 위험도 분류
|
|
90
114
|
|
|
91
115
|
각 권한에 [Destructive Command Guard (DCG)](https://github.com/Dicklesworthstone/destructive_command_guard)에서 영감을 받은 위험도가 부여됩니다. `--hey-claude-witness-me` 출력과 TUI 정보 모드에서 사용됩니다.
|
package/README.md
CHANGED
|
@@ -44,15 +44,15 @@ When running in a TTY (the default), ccperm opens a box-frame TUI:
|
|
|
44
44
|
**List view** — Projects sorted by permission count. Columns: Bash, MCP, Tools, TOTAL, `!` (risk), `†` (deprecated `:*`), `G` (redundant with global).
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
┌ ccperm
|
|
48
|
-
│ PROJECT
|
|
49
|
-
|
|
50
|
-
│ ~/.claude
|
|
51
|
-
|
|
52
|
-
│ ▸ my-project local
|
|
53
|
-
│ other-app shared
|
|
54
|
-
│
|
|
55
|
-
|
|
47
|
+
┌ ccperm ──────────────────────────────────────────── 1/8 ┐
|
|
48
|
+
│ PROJECT Bash MCP Tools TOTAL │
|
|
49
|
+
├─────────────────────────────────────────────────────────┤
|
|
50
|
+
│ ~/.claude 15 · 2 17 │
|
|
51
|
+
├─────────────────────────────────────────────────────────┤
|
|
52
|
+
│ ▸ my-project local 5 · 3 8 │
|
|
53
|
+
│ other-app shared 2 3 · 5 │
|
|
54
|
+
│ ! risk † deprecated G in global │
|
|
55
|
+
└──── [↑↓] navigate [Enter] detail [/] search [q] quit ┘
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
**Detail view** — Press Enter to expand a project. Categories are collapsible; press Enter to toggle.
|
|
@@ -86,6 +86,30 @@ ccperm distinguishes three levels of Claude Code settings:
|
|
|
86
86
|
|
|
87
87
|
Permissions are additive — global + shared + local are merged at runtime.
|
|
88
88
|
|
|
89
|
+
## Allow vs Deny vs Ask
|
|
90
|
+
|
|
91
|
+
Claude Code settings support `permissions.allow`, `permissions.deny`, and `permissions.ask`. ccperm separates them clearly:
|
|
92
|
+
|
|
93
|
+
- **Allow** permissions are shown in the main list and can be deleted (`[d]`) or copied to global (`[g]`)
|
|
94
|
+
- **Deny** rules appear in a collapsible **Deny** section in detail view, tagged with `DENY` and dimmed
|
|
95
|
+
- **Ask** rules appear in a collapsible **Ask** section — these are permissions that prompt for confirmation each time
|
|
96
|
+
- Deny and Ask rules **cannot be deleted or copied** — they are intentional controls
|
|
97
|
+
- `--verbose` output shows separate Deny and Ask sections per project
|
|
98
|
+
- `--hey-claude-witness-me` lists deny rules under "Protected rules"
|
|
99
|
+
- Allow counts never include deny or ask rules
|
|
100
|
+
|
|
101
|
+
## Additional Settings
|
|
102
|
+
|
|
103
|
+
ccperm also scans and displays these top-level settings when present:
|
|
104
|
+
|
|
105
|
+
| Field | Description |
|
|
106
|
+
|-------|-------------|
|
|
107
|
+
| `allowedTools` | Tools explicitly allowed for use (e.g. `Bash`, `Read`, `Write`) |
|
|
108
|
+
| `deniedTools` | Tools explicitly denied (e.g. `WebSearch`) |
|
|
109
|
+
| `additionalDirectories` | Extra directories Claude Code can access beyond the project root |
|
|
110
|
+
|
|
111
|
+
These appear as collapsible sections in the TUI detail view and in `--verbose` output. Like deny rules, they cannot be deleted or copied.
|
|
112
|
+
|
|
89
113
|
## Risk Classification
|
|
90
114
|
|
|
91
115
|
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.
|
package/dist/advisor.js
CHANGED
|
@@ -33,10 +33,13 @@ function analyze(results) {
|
|
|
33
33
|
}
|
|
34
34
|
const critical = findings.filter((f) => f.severity === 'critical');
|
|
35
35
|
const high = findings.filter((f) => f.severity === 'high');
|
|
36
|
+
// Deny stats
|
|
37
|
+
const totalDeny = results.reduce((sum, r) => sum + r.denyCount, 0);
|
|
36
38
|
// Header
|
|
37
39
|
lines.push(`# ccperm: Permission Audit`);
|
|
38
40
|
lines.push(``);
|
|
39
|
-
|
|
41
|
+
const denySuffix = totalDeny > 0 ? `, ${totalDeny} deny rules` : '';
|
|
42
|
+
lines.push(`Scanned ${results.length} settings files across ${dirs.size} projects. Found ${totalPerms} total permissions${denySuffix}.`);
|
|
40
43
|
lines.push(``);
|
|
41
44
|
// Severity summary
|
|
42
45
|
const counts = { critical: critical.length, high: high.length, medium: findings.filter((f) => f.severity === 'medium').length, low: findings.filter((f) => f.severity === 'low').length };
|
|
@@ -161,6 +164,29 @@ function analyze(results) {
|
|
|
161
164
|
}
|
|
162
165
|
lines.push(``);
|
|
163
166
|
}
|
|
167
|
+
// Protected rules (deny)
|
|
168
|
+
const denyFindings = [];
|
|
169
|
+
for (const r of results) {
|
|
170
|
+
const dir = (0, aggregator_js_1.projectDir)(r.display).replace(/.*\//, '');
|
|
171
|
+
for (const g of r.denyGroups) {
|
|
172
|
+
for (const item of g.items) {
|
|
173
|
+
denyFindings.push({ permission: item.name, project: dir });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (denyFindings.length > 0) {
|
|
178
|
+
lines.push(`## Protected rules (deny)`);
|
|
179
|
+
lines.push(`These deny rules actively block dangerous operations and should not be removed.`);
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
for (const f of denyFindings) {
|
|
182
|
+
const key = `${f.project}:${f.permission}`;
|
|
183
|
+
if (seen.has(key))
|
|
184
|
+
continue;
|
|
185
|
+
seen.add(key);
|
|
186
|
+
lines.push(`- \`${f.permission}\` in ${f.project}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push(``);
|
|
189
|
+
}
|
|
164
190
|
// How to act
|
|
165
191
|
lines.push(`## How to act`);
|
|
166
192
|
lines.push(`- Global settings: ~/.claude/settings.json`);
|
package/dist/aggregator.js
CHANGED
|
@@ -20,7 +20,7 @@ function toFileEntries(results) {
|
|
|
20
20
|
}
|
|
21
21
|
const fileType = r.isGlobal ? 'global' : r.display.includes('settings.local.json') ? 'local' : 'shared';
|
|
22
22
|
const name = r.isGlobal ? '~/.claude' : shortPath(r.display);
|
|
23
|
-
return { display: r.display, shortName: name, totalCount: r.totalCount, groups, isGlobal: r.isGlobal, fileType };
|
|
23
|
+
return { display: r.display, shortName: name, totalCount: r.totalCount, denyCount: r.denyCount, askCount: r.askCount, groups, isGlobal: r.isGlobal, fileType };
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
function summarize(results) {
|
|
@@ -34,11 +34,15 @@ function summarize(results) {
|
|
|
34
34
|
const projectsWithPerms = results.filter((r) => r.totalCount > 0).length;
|
|
35
35
|
const projectsEmpty = results.filter((r) => r.totalCount === 0).length;
|
|
36
36
|
const totalPerms = [...categoryTotals.values()].reduce((a, b) => a + b, 0);
|
|
37
|
+
const totalDeny = results.reduce((sum, r) => sum + r.denyCount, 0);
|
|
38
|
+
const totalAsk = results.reduce((sum, r) => sum + r.askCount, 0);
|
|
37
39
|
return {
|
|
38
40
|
totalProjects: dirs.size,
|
|
39
41
|
projectsWithPerms,
|
|
40
42
|
projectsEmpty,
|
|
41
43
|
totalPerms,
|
|
44
|
+
totalDeny,
|
|
45
|
+
totalAsk,
|
|
42
46
|
categoryTotals,
|
|
43
47
|
};
|
|
44
48
|
}
|
package/dist/interactive.js
CHANGED
|
@@ -78,6 +78,8 @@ function refreshProject(results, withPerms, idx, filePath) {
|
|
|
78
78
|
results[ri] = updated;
|
|
79
79
|
const entry = withPerms[idx];
|
|
80
80
|
entry.totalCount = updated.totalCount;
|
|
81
|
+
entry.denyCount = updated.denyCount;
|
|
82
|
+
entry.askCount = updated.askCount;
|
|
81
83
|
entry.groups = new Map();
|
|
82
84
|
for (const g of updated.groups)
|
|
83
85
|
entry.groups.set(g.category, g.items.length);
|
|
@@ -576,6 +578,82 @@ function renderDetail(state, withPerms, results, dupMap) {
|
|
|
576
578
|
}
|
|
577
579
|
}
|
|
578
580
|
}
|
|
581
|
+
// Deny section
|
|
582
|
+
if (fileResult.denyCount > 0) {
|
|
583
|
+
const denyKey = `${fileResult.path}:__deny__`;
|
|
584
|
+
const denyOpen = state.expanded.has(denyKey);
|
|
585
|
+
const denyArrow = denyOpen ? '▾' : '▸';
|
|
586
|
+
allNavRows.push({ text: `${colors_js_1.DIM}${denyArrow} Deny${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.denyCount})${colors_js_1.NC}`, key: denyKey, isDeny: true });
|
|
587
|
+
if (denyOpen) {
|
|
588
|
+
for (const group of fileResult.denyGroups) {
|
|
589
|
+
for (const item of group.items) {
|
|
590
|
+
const clean = cleanLabel(item.name);
|
|
591
|
+
const maxLen = w - 16;
|
|
592
|
+
const name = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '…' : clean;
|
|
593
|
+
allNavRows.push({ text: ` ${colors_js_1.DIM}DENY ${name}${colors_js_1.NC}`, isDeny: true });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Ask section
|
|
599
|
+
if (fileResult.askCount > 0) {
|
|
600
|
+
const askKey = `${fileResult.path}:__ask__`;
|
|
601
|
+
const askOpen = state.expanded.has(askKey);
|
|
602
|
+
const askArrow = askOpen ? '▾' : '▸';
|
|
603
|
+
allNavRows.push({ text: `${colors_js_1.YELLOW}${askArrow} Ask${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.askCount})${colors_js_1.NC}`, key: askKey, isDeny: true });
|
|
604
|
+
if (askOpen) {
|
|
605
|
+
for (const group of fileResult.askGroups) {
|
|
606
|
+
for (const item of group.items) {
|
|
607
|
+
const clean = cleanLabel(item.name);
|
|
608
|
+
const maxLen = w - 16;
|
|
609
|
+
const name = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '…' : clean;
|
|
610
|
+
allNavRows.push({ text: ` ${colors_js_1.DIM}ASK ${name}${colors_js_1.NC}`, isDeny: true });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// AllowedTools section
|
|
616
|
+
if (fileResult.allowedTools.length > 0) {
|
|
617
|
+
const atKey = `${fileResult.path}:__allowedTools__`;
|
|
618
|
+
const atOpen = state.expanded.has(atKey);
|
|
619
|
+
const atArrow = atOpen ? '▾' : '▸';
|
|
620
|
+
allNavRows.push({ text: `${colors_js_1.CYAN}${atArrow} AllowedTools${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.allowedTools.length})${colors_js_1.NC}`, key: atKey, isDeny: true });
|
|
621
|
+
if (atOpen) {
|
|
622
|
+
for (const t of fileResult.allowedTools) {
|
|
623
|
+
const maxLen = w - 10;
|
|
624
|
+
const name = t.length > maxLen ? t.slice(0, maxLen - 1) + '…' : t;
|
|
625
|
+
allNavRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, isDeny: true });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// DeniedTools section
|
|
630
|
+
if (fileResult.deniedTools.length > 0) {
|
|
631
|
+
const dtKey = `${fileResult.path}:__deniedTools__`;
|
|
632
|
+
const dtOpen = state.expanded.has(dtKey);
|
|
633
|
+
const dtArrow = dtOpen ? '▾' : '▸';
|
|
634
|
+
allNavRows.push({ text: `${colors_js_1.RED}${dtArrow} DeniedTools${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.deniedTools.length})${colors_js_1.NC}`, key: dtKey, isDeny: true });
|
|
635
|
+
if (dtOpen) {
|
|
636
|
+
for (const t of fileResult.deniedTools) {
|
|
637
|
+
const maxLen = w - 10;
|
|
638
|
+
const name = t.length > maxLen ? t.slice(0, maxLen - 1) + '…' : t;
|
|
639
|
+
allNavRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, isDeny: true });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// AdditionalDirectories section
|
|
644
|
+
if (fileResult.additionalDirectories.length > 0) {
|
|
645
|
+
const adKey = `${fileResult.path}:__additionalDirs__`;
|
|
646
|
+
const adOpen = state.expanded.has(adKey);
|
|
647
|
+
const adArrow = adOpen ? '▾' : '▸';
|
|
648
|
+
allNavRows.push({ text: `${colors_js_1.DIM}${adArrow} AdditionalDirectories${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.additionalDirectories.length})${colors_js_1.NC}`, key: adKey, isDeny: true });
|
|
649
|
+
if (adOpen) {
|
|
650
|
+
for (const d of fileResult.additionalDirectories) {
|
|
651
|
+
const maxLen = w - 10;
|
|
652
|
+
const name = d.length > maxLen ? d.slice(0, maxLen - 1) + '…' : d;
|
|
653
|
+
allNavRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, isDeny: true });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
579
657
|
const navRows = allNavRows;
|
|
580
658
|
// handle toggle
|
|
581
659
|
if (state._toggle) {
|
|
@@ -594,7 +672,10 @@ function renderDetail(state, withPerms, results, dupMap) {
|
|
|
594
672
|
if (state._delete) {
|
|
595
673
|
delete state._delete;
|
|
596
674
|
const row = navRows[state.detailCursor];
|
|
597
|
-
if (row?.
|
|
675
|
+
if (row?.isDeny) {
|
|
676
|
+
state.flash = `${colors_js_1.DIM}· Deny rules cannot be deleted${colors_js_1.NC}`;
|
|
677
|
+
}
|
|
678
|
+
else if (row?.rawPerm) {
|
|
598
679
|
state.confirmDelete = { perm: row.perm, rawPerm: row.rawPerm, filePath: fileResult.path };
|
|
599
680
|
}
|
|
600
681
|
}
|
|
@@ -602,7 +683,10 @@ function renderDetail(state, withPerms, results, dupMap) {
|
|
|
602
683
|
if (state._global) {
|
|
603
684
|
delete state._global;
|
|
604
685
|
const row = navRows[state.detailCursor];
|
|
605
|
-
if (row?.
|
|
686
|
+
if (row?.isDeny) {
|
|
687
|
+
state.flash = `${colors_js_1.DIM}· Deny rules cannot be copied${colors_js_1.NC}`;
|
|
688
|
+
}
|
|
689
|
+
else if (row?.rawPerm && !project.isGlobal) {
|
|
606
690
|
state.confirmGlobal = { perm: row.perm, rawPerm: row.rawPerm };
|
|
607
691
|
}
|
|
608
692
|
}
|
package/dist/renderer.js
CHANGED
|
@@ -59,6 +59,40 @@ function printVerbose(results, summary) {
|
|
|
59
59
|
console.log(` ${colors_js_1.DIM}${item.name}${colors_js_1.NC}`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
if (result.denyCount > 0) {
|
|
63
|
+
console.log(` ${colors_js_1.RED}Deny${colors_js_1.NC} ${colors_js_1.DIM}(${result.denyCount})${colors_js_1.NC}`);
|
|
64
|
+
for (const group of result.denyGroups) {
|
|
65
|
+
for (const item of group.items) {
|
|
66
|
+
console.log(` ${colors_js_1.DIM}DENY ${item.name}${colors_js_1.NC}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (result.askCount > 0) {
|
|
71
|
+
console.log(` ${colors_js_1.YELLOW}Ask${colors_js_1.NC} ${colors_js_1.DIM}(${result.askCount})${colors_js_1.NC}`);
|
|
72
|
+
for (const group of result.askGroups) {
|
|
73
|
+
for (const item of group.items) {
|
|
74
|
+
console.log(` ${colors_js_1.DIM}ASK ${item.name}${colors_js_1.NC}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (result.allowedTools.length > 0) {
|
|
79
|
+
console.log(` ${colors_js_1.CYAN}AllowedTools${colors_js_1.NC} ${colors_js_1.DIM}(${result.allowedTools.length})${colors_js_1.NC}`);
|
|
80
|
+
for (const t of result.allowedTools) {
|
|
81
|
+
console.log(` ${colors_js_1.DIM}${t}${colors_js_1.NC}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (result.deniedTools.length > 0) {
|
|
85
|
+
console.log(` ${colors_js_1.RED}DeniedTools${colors_js_1.NC} ${colors_js_1.DIM}(${result.deniedTools.length})${colors_js_1.NC}`);
|
|
86
|
+
for (const t of result.deniedTools) {
|
|
87
|
+
console.log(` ${colors_js_1.DIM}${t}${colors_js_1.NC}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (result.additionalDirectories.length > 0) {
|
|
91
|
+
console.log(` ${colors_js_1.DIM}AdditionalDirectories${colors_js_1.NC} ${colors_js_1.DIM}(${result.additionalDirectories.length})${colors_js_1.NC}`);
|
|
92
|
+
for (const d of result.additionalDirectories) {
|
|
93
|
+
console.log(` ${colors_js_1.DIM}${d}${colors_js_1.NC}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
62
96
|
console.log('');
|
|
63
97
|
}
|
|
64
98
|
if (projectsEmpty.length > 0) {
|
|
@@ -68,5 +102,7 @@ function printVerbose(results, summary) {
|
|
|
68
102
|
function printFooter(summary) {
|
|
69
103
|
console.log(`\n ${colors_js_1.DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors_js_1.NC}`);
|
|
70
104
|
const catSummary = [...summary.categoryTotals.entries()].map(([k, v]) => `${k}: ${colors_js_1.BOLD}${v}${colors_js_1.NC}${colors_js_1.DIM}`).join(' ');
|
|
71
|
-
|
|
105
|
+
const denySuffix = summary.totalDeny > 0 ? ` ${colors_js_1.BOLD}${summary.totalDeny}${colors_js_1.NC} deny` : '';
|
|
106
|
+
const askSuffix = summary.totalAsk > 0 ? ` ${colors_js_1.BOLD}${summary.totalAsk}${colors_js_1.NC} ask` : '';
|
|
107
|
+
console.log(` ${colors_js_1.BOLD}${summary.totalProjects}${colors_js_1.NC} projects ${colors_js_1.BOLD}${summary.totalPerms}${colors_js_1.NC} permissions${denySuffix}${askSuffix} ${colors_js_1.DIM}(${catSummary})${colors_js_1.NC}\n`);
|
|
72
108
|
}
|
package/dist/scanner.js
CHANGED
|
@@ -12,7 +12,6 @@ exports.scanFile = scanFile;
|
|
|
12
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
14
|
const node_os_1 = __importDefault(require("node:os"));
|
|
15
|
-
const PERM_RE = /"(Bash|Write|Edit|Read|Glob|Grep|WebSearch|WebFetch|mcp_)[^"]*"/g;
|
|
16
15
|
const DEPRECATED_RE = /:\*\)|:\*"/g;
|
|
17
16
|
const AUDIT_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.ccperm', 'audit');
|
|
18
17
|
function writeAudit(action, filePath, perm, before, after) {
|
|
@@ -202,10 +201,26 @@ function scanFile(filePath) {
|
|
|
202
201
|
catch {
|
|
203
202
|
return null;
|
|
204
203
|
}
|
|
205
|
-
|
|
204
|
+
let json;
|
|
205
|
+
try {
|
|
206
|
+
json = JSON.parse(content);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const allowArr = Array.isArray(json?.permissions?.allow) ? json.permissions.allow : [];
|
|
212
|
+
const denyArr = Array.isArray(json?.permissions?.deny) ? json.permissions.deny : [];
|
|
213
|
+
const askArr = Array.isArray(json?.permissions?.ask) ? json.permissions.ask : [];
|
|
214
|
+
const perms = [...new Set(allowArr)].sort();
|
|
215
|
+
const denyPerms = [...new Set(denyArr)].sort();
|
|
216
|
+
const askPerms = [...new Set(askArr)].sort();
|
|
206
217
|
const groups = groupPermissions(perms);
|
|
207
|
-
const
|
|
208
|
-
|
|
218
|
+
const denyGroups = groupPermissions(denyPerms);
|
|
219
|
+
const askGroups = groupPermissions(askPerms);
|
|
220
|
+
const allowedTools = Array.isArray(json?.allowedTools) ? [...new Set(json.allowedTools)].sort() : [];
|
|
221
|
+
const deniedTools = Array.isArray(json?.deniedTools) ? [...new Set(json.deniedTools)].sort() : [];
|
|
222
|
+
const additionalDirectories = Array.isArray(json?.additionalDirectories) ? [...new Set(json.additionalDirectories)].sort() : [];
|
|
223
|
+
return { path: filePath, display, permissions: perms, groups, totalCount: perms.length, denyPermissions: denyPerms, denyGroups, denyCount: denyPerms.length, askPermissions: askPerms, askGroups, askCount: askPerms.length, allowedTools, deniedTools, additionalDirectories, isGlobal };
|
|
209
224
|
}
|
|
210
225
|
function categorize(perm) {
|
|
211
226
|
if (perm.startsWith('Bash')) {
|