claude-cli-advanced-starter-pack 1.0.16 → 1.8.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/OVERVIEW.md +5 -1
- package/README.md +241 -132
- package/bin/gtask.js +53 -0
- package/package.json +1 -1
- package/src/cli/menu.js +27 -0
- package/src/commands/explore-mcp/mcp-registry.js +99 -0
- package/src/commands/init.js +309 -80
- package/src/commands/install-panel-hook.js +108 -0
- package/src/commands/install-scripts.js +232 -0
- package/src/commands/install-skill.js +220 -0
- package/src/commands/panel.js +297 -0
- package/src/commands/setup-wizard.js +4 -3
- package/src/commands/test-setup.js +4 -5
- package/src/data/releases.json +209 -0
- package/src/panel/queue.js +188 -0
- package/templates/commands/ask-claude.template.md +118 -0
- package/templates/commands/ccasp-panel.template.md +72 -0
- package/templates/commands/ccasp-setup.template.md +470 -79
- package/templates/commands/create-smoke-test.template.md +186 -0
- package/templates/commands/project-impl.template.md +9 -113
- package/templates/commands/refactor-check.template.md +112 -0
- package/templates/commands/refactor-cleanup.template.md +144 -0
- package/templates/commands/refactor-prep.template.md +192 -0
- package/templates/docs/AI_ARCHITECTURE_CONSTITUTION.template.md +198 -0
- package/templates/docs/DETAILED_GOTCHAS.template.md +347 -0
- package/templates/docs/PHASE-DEV-CHECKLIST.template.md +241 -0
- package/templates/docs/PROGRESS_JSON_TEMPLATE.json +117 -0
- package/templates/docs/background-agent.template.md +264 -0
- package/templates/hooks/autonomous-decision-logger.template.js +207 -0
- package/templates/hooks/branch-merge-checker.template.js +272 -0
- package/templates/hooks/context-injector.template.js +261 -0
- package/templates/hooks/git-commit-tracker.template.js +267 -0
- package/templates/hooks/happy-mode-detector.template.js +214 -0
- package/templates/hooks/happy-title-generator.template.js +260 -0
- package/templates/hooks/issue-completion-detector.template.js +205 -0
- package/templates/hooks/panel-queue-reader.template.js +83 -0
- package/templates/hooks/phase-validation-gates.template.js +307 -0
- package/templates/hooks/session-id-generator.template.js +236 -0
- package/templates/hooks/token-budget-loader.template.js +234 -0
- package/templates/hooks/token-usage-monitor.template.js +193 -0
- package/templates/hooks/tool-output-cacher.template.js +219 -0
- package/templates/patterns/README.md +129 -0
- package/templates/patterns/l1-l2-orchestration.md +189 -0
- package/templates/patterns/multi-phase-orchestration.md +258 -0
- package/templates/patterns/two-tier-query-pipeline.md +192 -0
- package/templates/scripts/README.md +109 -0
- package/templates/scripts/analyze-delegation-log.js +299 -0
- package/templates/scripts/autonomous-decision-logger.js +277 -0
- package/templates/scripts/git-history-analyzer.py +269 -0
- package/templates/scripts/phase-validation-gates.js +307 -0
- package/templates/scripts/poll-deployment-status.js +260 -0
- package/templates/scripts/roadmap-scanner.js +263 -0
- package/templates/scripts/validate-deployment.js +293 -0
- package/templates/skills/agent-creator/skill.json +18 -0
- package/templates/skills/agent-creator/skill.md +335 -0
- package/templates/skills/hook-creator/skill.json +18 -0
- package/templates/skills/hook-creator/skill.md +318 -0
- package/templates/skills/panel/skill.json +18 -0
- package/templates/skills/panel/skill.md +90 -0
- package/templates/skills/rag-agent-creator/skill.json +18 -0
- package/templates/skills/rag-agent-creator/skill.md +307 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Autonomous Decision Logger
|
|
4
|
+
*
|
|
5
|
+
* Creates JSONL audit trail for agent decisions.
|
|
6
|
+
* Tracks reasoning, confidence, and outcomes.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* // Import in your hook or script
|
|
10
|
+
* import { DecisionLogger } from './autonomous-decision-logger.js';
|
|
11
|
+
*
|
|
12
|
+
* const logger = new DecisionLogger('.claude/logs/decisions.jsonl');
|
|
13
|
+
* logger.logDecision({
|
|
14
|
+
* agent: 'deployment-checker',
|
|
15
|
+
* decision: 'deploy',
|
|
16
|
+
* reasoning: 'All tests passed, no conflicts',
|
|
17
|
+
* confidence: 0.95,
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
|
|
22
|
+
import { dirname, resolve } from 'path';
|
|
23
|
+
|
|
24
|
+
export class DecisionLogger {
|
|
25
|
+
constructor(logPath = '.claude/logs/decisions.jsonl') {
|
|
26
|
+
this.logPath = resolve(process.cwd(), logPath);
|
|
27
|
+
this.sessionId = this.generateSessionId();
|
|
28
|
+
this.ensureLogDirectory();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
generateSessionId() {
|
|
32
|
+
const timestamp = Date.now().toString(36);
|
|
33
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
34
|
+
return `${timestamp}-${random}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ensureLogDirectory() {
|
|
38
|
+
const dir = dirname(this.logPath);
|
|
39
|
+
if (!existsSync(dir)) {
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Log a decision with full context
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} decision
|
|
48
|
+
* @param {string} decision.agent - Agent/component making decision
|
|
49
|
+
* @param {string} decision.decision - The decision made
|
|
50
|
+
* @param {string} decision.reasoning - Why this decision was made
|
|
51
|
+
* @param {number} decision.confidence - Confidence level 0-1
|
|
52
|
+
* @param {Object} [decision.context] - Additional context
|
|
53
|
+
* @param {string} [decision.outcome] - Result (success/failure/pending)
|
|
54
|
+
* @param {Array} [decision.alternatives] - Other options considered
|
|
55
|
+
*/
|
|
56
|
+
logDecision(decision) {
|
|
57
|
+
const entry = {
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
sessionId: this.sessionId,
|
|
60
|
+
pid: process.pid,
|
|
61
|
+
...decision,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this.writeEntry(entry);
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Log an action taken by an agent
|
|
70
|
+
*/
|
|
71
|
+
logAction(agent, action, details = {}) {
|
|
72
|
+
return this.logDecision({
|
|
73
|
+
agent,
|
|
74
|
+
type: 'action',
|
|
75
|
+
decision: action,
|
|
76
|
+
...details,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Log an observation or analysis
|
|
82
|
+
*/
|
|
83
|
+
logObservation(agent, observation, details = {}) {
|
|
84
|
+
return this.logDecision({
|
|
85
|
+
agent,
|
|
86
|
+
type: 'observation',
|
|
87
|
+
decision: observation,
|
|
88
|
+
...details,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Log an error or failure
|
|
94
|
+
*/
|
|
95
|
+
logError(agent, error, context = {}) {
|
|
96
|
+
return this.logDecision({
|
|
97
|
+
agent,
|
|
98
|
+
type: 'error',
|
|
99
|
+
decision: 'error',
|
|
100
|
+
reasoning: error.message || error,
|
|
101
|
+
outcome: 'failure',
|
|
102
|
+
context: {
|
|
103
|
+
...context,
|
|
104
|
+
stack: error.stack,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Log a phase transition
|
|
111
|
+
*/
|
|
112
|
+
logPhaseTransition(fromPhase, toPhase, reason, validation = {}) {
|
|
113
|
+
return this.logDecision({
|
|
114
|
+
agent: 'phase-controller',
|
|
115
|
+
type: 'phase_transition',
|
|
116
|
+
decision: `${fromPhase} -> ${toPhase}`,
|
|
117
|
+
reasoning: reason,
|
|
118
|
+
context: { fromPhase, toPhase, validation },
|
|
119
|
+
outcome: 'success',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log agent spawn/delegation
|
|
125
|
+
*/
|
|
126
|
+
logDelegation(parentAgent, childAgent, task, options = {}) {
|
|
127
|
+
return this.logDecision({
|
|
128
|
+
agent: parentAgent,
|
|
129
|
+
type: 'delegation',
|
|
130
|
+
decision: `spawn:${childAgent}`,
|
|
131
|
+
reasoning: `Delegating: ${task}`,
|
|
132
|
+
context: {
|
|
133
|
+
childAgent,
|
|
134
|
+
task,
|
|
135
|
+
model: options.model,
|
|
136
|
+
runInBackground: options.runInBackground,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
writeEntry(entry) {
|
|
142
|
+
const line = JSON.stringify(entry) + '\n';
|
|
143
|
+
appendFileSync(this.logPath, line);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Read all decisions for current session
|
|
148
|
+
*/
|
|
149
|
+
getSessionDecisions() {
|
|
150
|
+
if (!existsSync(this.logPath)) return [];
|
|
151
|
+
|
|
152
|
+
const content = readFileSync(this.logPath, 'utf8');
|
|
153
|
+
return content
|
|
154
|
+
.split('\n')
|
|
155
|
+
.filter(line => line.trim())
|
|
156
|
+
.map(line => JSON.parse(line))
|
|
157
|
+
.filter(entry => entry.sessionId === this.sessionId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get summary statistics
|
|
162
|
+
*/
|
|
163
|
+
getSummary() {
|
|
164
|
+
const decisions = this.getSessionDecisions();
|
|
165
|
+
|
|
166
|
+
const summary = {
|
|
167
|
+
sessionId: this.sessionId,
|
|
168
|
+
totalDecisions: decisions.length,
|
|
169
|
+
byType: {},
|
|
170
|
+
byAgent: {},
|
|
171
|
+
byOutcome: {},
|
|
172
|
+
avgConfidence: 0,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let confidenceSum = 0;
|
|
176
|
+
let confidenceCount = 0;
|
|
177
|
+
|
|
178
|
+
for (const d of decisions) {
|
|
179
|
+
// Count by type
|
|
180
|
+
const type = d.type || 'decision';
|
|
181
|
+
summary.byType[type] = (summary.byType[type] || 0) + 1;
|
|
182
|
+
|
|
183
|
+
// Count by agent
|
|
184
|
+
summary.byAgent[d.agent] = (summary.byAgent[d.agent] || 0) + 1;
|
|
185
|
+
|
|
186
|
+
// Count by outcome
|
|
187
|
+
if (d.outcome) {
|
|
188
|
+
summary.byOutcome[d.outcome] = (summary.byOutcome[d.outcome] || 0) + 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Average confidence
|
|
192
|
+
if (typeof d.confidence === 'number') {
|
|
193
|
+
confidenceSum += d.confidence;
|
|
194
|
+
confidenceCount++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (confidenceCount > 0) {
|
|
199
|
+
summary.avgConfidence = (confidenceSum / confidenceCount).toFixed(2);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return summary;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// CLI mode - print summary if run directly
|
|
207
|
+
async function main() {
|
|
208
|
+
const args = process.argv.slice(2);
|
|
209
|
+
const logPath = args[0] || '.claude/logs/decisions.jsonl';
|
|
210
|
+
|
|
211
|
+
if (!existsSync(logPath)) {
|
|
212
|
+
console.log(`No log file found at: ${logPath}`);
|
|
213
|
+
console.log('\nUsage: node autonomous-decision-logger.js [log-path]');
|
|
214
|
+
console.log('\nTo create logs, import DecisionLogger in your code:');
|
|
215
|
+
console.log(" import { DecisionLogger } from './autonomous-decision-logger.js';");
|
|
216
|
+
console.log(" const logger = new DecisionLogger();");
|
|
217
|
+
console.log(" logger.logDecision({ agent: 'my-agent', decision: 'proceed', confidence: 0.9 });");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Read and summarize log
|
|
222
|
+
const content = readFileSync(logPath, 'utf8');
|
|
223
|
+
const entries = content
|
|
224
|
+
.split('\n')
|
|
225
|
+
.filter(line => line.trim())
|
|
226
|
+
.map(line => JSON.parse(line));
|
|
227
|
+
|
|
228
|
+
console.log('\n📋 Decision Log Summary\n');
|
|
229
|
+
console.log(`Log file: ${logPath}`);
|
|
230
|
+
console.log(`Total entries: ${entries.length}`);
|
|
231
|
+
|
|
232
|
+
// Group by session
|
|
233
|
+
const sessions = {};
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
const sid = entry.sessionId || 'unknown';
|
|
236
|
+
if (!sessions[sid]) {
|
|
237
|
+
sessions[sid] = [];
|
|
238
|
+
}
|
|
239
|
+
sessions[sid].push(entry);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(`Sessions: ${Object.keys(sessions).length}`);
|
|
243
|
+
|
|
244
|
+
// Show last 10 decisions
|
|
245
|
+
console.log('\n📜 Last 10 Decisions:\n');
|
|
246
|
+
const recent = entries.slice(-10);
|
|
247
|
+
for (const entry of recent) {
|
|
248
|
+
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
249
|
+
const conf = entry.confidence ? ` (${(entry.confidence * 100).toFixed(0)}%)` : '';
|
|
250
|
+
console.log(` [${time}] ${entry.agent}: ${entry.decision}${conf}`);
|
|
251
|
+
if (entry.reasoning) {
|
|
252
|
+
console.log(` └─ ${entry.reasoning.substring(0, 60)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// By type
|
|
257
|
+
const typeCount = {};
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
const type = entry.type || 'decision';
|
|
260
|
+
typeCount[type] = (typeCount[type] || 0) + 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log('\n📊 By Type:');
|
|
264
|
+
for (const [type, count] of Object.entries(typeCount).sort((a, b) => b[1] - a[1])) {
|
|
265
|
+
console.log(` ${type}: ${count}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log('');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Export for use as module
|
|
272
|
+
export default DecisionLogger;
|
|
273
|
+
|
|
274
|
+
// Run CLI if called directly
|
|
275
|
+
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`) {
|
|
276
|
+
main().catch(console.error);
|
|
277
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Git History Analyzer
|
|
4
|
+
|
|
5
|
+
Security audit for sensitive data in git history.
|
|
6
|
+
Scans commits for passwords, API keys, tokens, and other secrets.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python git-history-analyzer.py
|
|
10
|
+
python git-history-analyzer.py --patterns "password|secret|api.key"
|
|
11
|
+
python git-history-analyzer.py --since "2024-01-01" --output json
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
import argparse
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# Default patterns for sensitive data
|
|
23
|
+
DEFAULT_PATTERNS = [
|
|
24
|
+
# API keys and tokens
|
|
25
|
+
r'(?i)(api[_-]?key|apikey)\s*[:=]\s*["\']?[\w-]{20,}',
|
|
26
|
+
r'(?i)(access[_-]?token|auth[_-]?token)\s*[:=]\s*["\']?[\w-]{20,}',
|
|
27
|
+
r'(?i)(secret[_-]?key|private[_-]?key)\s*[:=]\s*["\']?[\w-]{20,}',
|
|
28
|
+
|
|
29
|
+
# Passwords
|
|
30
|
+
r'(?i)password\s*[:=]\s*["\'][^"\']{4,}["\']',
|
|
31
|
+
r'(?i)passwd\s*[:=]\s*["\'][^"\']{4,}["\']',
|
|
32
|
+
|
|
33
|
+
# AWS
|
|
34
|
+
r'AKIA[0-9A-Z]{16}', # AWS Access Key
|
|
35
|
+
r'(?i)aws[_-]?secret[_-]?access[_-]?key\s*[:=]',
|
|
36
|
+
|
|
37
|
+
# GitHub
|
|
38
|
+
r'ghp_[a-zA-Z0-9]{36}', # GitHub Personal Access Token
|
|
39
|
+
r'github[_-]?token\s*[:=]',
|
|
40
|
+
|
|
41
|
+
# Generic secrets
|
|
42
|
+
r'(?i)bearer\s+[a-zA-Z0-9\-._~+/]+=*',
|
|
43
|
+
r'(?i)-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----',
|
|
44
|
+
|
|
45
|
+
# Database URLs
|
|
46
|
+
r'(?i)(postgres|mysql|mongodb|redis)://[^\s"\']+',
|
|
47
|
+
|
|
48
|
+
# Environment variables with sensitive names
|
|
49
|
+
r'(?i)(DB_PASSWORD|DATABASE_PASSWORD|MYSQL_ROOT_PASSWORD)\s*=',
|
|
50
|
+
r'(?i)(JWT_SECRET|SESSION_SECRET|ENCRYPTION_KEY)\s*=',
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GitHistoryAnalyzer:
|
|
55
|
+
def __init__(self, patterns=None, since=None, verbose=False):
|
|
56
|
+
self.patterns = patterns or DEFAULT_PATTERNS
|
|
57
|
+
self.compiled_patterns = [re.compile(p) for p in self.patterns]
|
|
58
|
+
self.since = since
|
|
59
|
+
self.verbose = verbose
|
|
60
|
+
self.findings = []
|
|
61
|
+
|
|
62
|
+
def run(self):
|
|
63
|
+
"""Run the analysis."""
|
|
64
|
+
print("\n🔍 Git History Security Audit\n")
|
|
65
|
+
|
|
66
|
+
if not self._is_git_repo():
|
|
67
|
+
print("❌ Not a git repository")
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
print(f"Scanning {len(self.patterns)} patterns...")
|
|
71
|
+
if self.since:
|
|
72
|
+
print(f"Since: {self.since}")
|
|
73
|
+
print()
|
|
74
|
+
|
|
75
|
+
commits = self._get_commits()
|
|
76
|
+
print(f"Found {len(commits)} commits to analyze\n")
|
|
77
|
+
|
|
78
|
+
for i, commit in enumerate(commits, 1):
|
|
79
|
+
if self.verbose:
|
|
80
|
+
print(f"[{i}/{len(commits)}] {commit['hash'][:8]}", end='\r')
|
|
81
|
+
self._analyze_commit(commit)
|
|
82
|
+
|
|
83
|
+
if self.verbose:
|
|
84
|
+
print()
|
|
85
|
+
|
|
86
|
+
return self.findings
|
|
87
|
+
|
|
88
|
+
def _is_git_repo(self):
|
|
89
|
+
"""Check if current directory is a git repository."""
|
|
90
|
+
try:
|
|
91
|
+
subprocess.run(
|
|
92
|
+
['git', 'rev-parse', '--git-dir'],
|
|
93
|
+
capture_output=True,
|
|
94
|
+
check=True
|
|
95
|
+
)
|
|
96
|
+
return True
|
|
97
|
+
except subprocess.CalledProcessError:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def _get_commits(self):
|
|
101
|
+
"""Get list of commits to analyze."""
|
|
102
|
+
cmd = ['git', 'log', '--pretty=format:%H|%an|%ae|%ad|%s', '--date=iso']
|
|
103
|
+
|
|
104
|
+
if self.since:
|
|
105
|
+
cmd.extend(['--since', self.since])
|
|
106
|
+
|
|
107
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
108
|
+
commits = []
|
|
109
|
+
|
|
110
|
+
for line in result.stdout.strip().split('\n'):
|
|
111
|
+
if line:
|
|
112
|
+
parts = line.split('|', 4)
|
|
113
|
+
if len(parts) == 5:
|
|
114
|
+
commits.append({
|
|
115
|
+
'hash': parts[0],
|
|
116
|
+
'author': parts[1],
|
|
117
|
+
'email': parts[2],
|
|
118
|
+
'date': parts[3],
|
|
119
|
+
'message': parts[4],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return commits
|
|
123
|
+
|
|
124
|
+
def _analyze_commit(self, commit):
|
|
125
|
+
"""Analyze a single commit for sensitive data."""
|
|
126
|
+
# Get diff for this commit
|
|
127
|
+
cmd = ['git', 'show', '--pretty=', '--diff-filter=AM', commit['hash']]
|
|
128
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
129
|
+
|
|
130
|
+
diff = result.stdout
|
|
131
|
+
|
|
132
|
+
# Check each pattern
|
|
133
|
+
for i, pattern in enumerate(self.compiled_patterns):
|
|
134
|
+
matches = pattern.findall(diff)
|
|
135
|
+
if matches:
|
|
136
|
+
for match in matches:
|
|
137
|
+
finding = {
|
|
138
|
+
'commit': commit['hash'],
|
|
139
|
+
'author': commit['author'],
|
|
140
|
+
'date': commit['date'],
|
|
141
|
+
'message': commit['message'],
|
|
142
|
+
'pattern': self.patterns[i],
|
|
143
|
+
'match': match if isinstance(match, str) else match[0],
|
|
144
|
+
'severity': self._get_severity(self.patterns[i]),
|
|
145
|
+
}
|
|
146
|
+
self.findings.append(finding)
|
|
147
|
+
|
|
148
|
+
def _get_severity(self, pattern):
|
|
149
|
+
"""Determine severity based on pattern type."""
|
|
150
|
+
high_severity = ['password', 'private.key', 'aws', 'secret']
|
|
151
|
+
medium_severity = ['api.key', 'token', 'bearer']
|
|
152
|
+
|
|
153
|
+
pattern_lower = pattern.lower()
|
|
154
|
+
|
|
155
|
+
if any(s in pattern_lower for s in high_severity):
|
|
156
|
+
return 'HIGH'
|
|
157
|
+
elif any(s in pattern_lower for s in medium_severity):
|
|
158
|
+
return 'MEDIUM'
|
|
159
|
+
else:
|
|
160
|
+
return 'LOW'
|
|
161
|
+
|
|
162
|
+
def print_report(self):
|
|
163
|
+
"""Print human-readable report."""
|
|
164
|
+
if not self.findings:
|
|
165
|
+
print("✅ No sensitive data found in git history\n")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
print(f"\n⚠️ Found {len(self.findings)} potential issues\n")
|
|
169
|
+
print("=" * 70)
|
|
170
|
+
|
|
171
|
+
# Group by severity
|
|
172
|
+
by_severity = {'HIGH': [], 'MEDIUM': [], 'LOW': []}
|
|
173
|
+
for finding in self.findings:
|
|
174
|
+
by_severity[finding['severity']].append(finding)
|
|
175
|
+
|
|
176
|
+
for severity in ['HIGH', 'MEDIUM', 'LOW']:
|
|
177
|
+
findings = by_severity[severity]
|
|
178
|
+
if findings:
|
|
179
|
+
icon = '🔴' if severity == 'HIGH' else '🟡' if severity == 'MEDIUM' else '🔵'
|
|
180
|
+
print(f"\n{icon} {severity} Severity ({len(findings)} issues)\n")
|
|
181
|
+
|
|
182
|
+
for f in findings[:10]: # Limit to 10 per severity
|
|
183
|
+
print(f" Commit: {f['commit'][:8]}")
|
|
184
|
+
print(f" Date: {f['date']}")
|
|
185
|
+
print(f" Author: {f['author']}")
|
|
186
|
+
print(f" Match: {f['match'][:50]}...")
|
|
187
|
+
print()
|
|
188
|
+
|
|
189
|
+
if len(findings) > 10:
|
|
190
|
+
print(f" ... and {len(findings) - 10} more\n")
|
|
191
|
+
|
|
192
|
+
print("=" * 70)
|
|
193
|
+
print("\n📋 Recommendations:\n")
|
|
194
|
+
|
|
195
|
+
if by_severity['HIGH']:
|
|
196
|
+
print(" 1. Immediately rotate any exposed credentials")
|
|
197
|
+
print(" 2. Consider using git-filter-repo to remove sensitive commits")
|
|
198
|
+
print(" 3. Add patterns to .gitignore and pre-commit hooks")
|
|
199
|
+
|
|
200
|
+
print(" 4. Use environment variables for secrets")
|
|
201
|
+
print(" 5. Consider using a secrets manager (Vault, AWS Secrets Manager)")
|
|
202
|
+
print()
|
|
203
|
+
|
|
204
|
+
def to_json(self):
|
|
205
|
+
"""Return findings as JSON."""
|
|
206
|
+
return json.dumps({
|
|
207
|
+
'scannedAt': datetime.now().isoformat(),
|
|
208
|
+
'totalFindings': len(self.findings),
|
|
209
|
+
'bySeverity': {
|
|
210
|
+
'HIGH': len([f for f in self.findings if f['severity'] == 'HIGH']),
|
|
211
|
+
'MEDIUM': len([f for f in self.findings if f['severity'] == 'MEDIUM']),
|
|
212
|
+
'LOW': len([f for f in self.findings if f['severity'] == 'LOW']),
|
|
213
|
+
},
|
|
214
|
+
'findings': self.findings,
|
|
215
|
+
}, indent=2)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main():
|
|
219
|
+
parser = argparse.ArgumentParser(
|
|
220
|
+
description='Scan git history for sensitive data'
|
|
221
|
+
)
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
'--patterns',
|
|
224
|
+
help='Custom regex patterns (pipe-separated)',
|
|
225
|
+
default=None
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
'--since',
|
|
229
|
+
help='Only scan commits since date (e.g., "2024-01-01")',
|
|
230
|
+
default=None
|
|
231
|
+
)
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
'--output',
|
|
234
|
+
choices=['text', 'json'],
|
|
235
|
+
default='text',
|
|
236
|
+
help='Output format'
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
'--verbose', '-v',
|
|
240
|
+
action='store_true',
|
|
241
|
+
help='Show progress'
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
args = parser.parse_args()
|
|
245
|
+
|
|
246
|
+
patterns = None
|
|
247
|
+
if args.patterns:
|
|
248
|
+
patterns = args.patterns.split('|')
|
|
249
|
+
|
|
250
|
+
analyzer = GitHistoryAnalyzer(
|
|
251
|
+
patterns=patterns,
|
|
252
|
+
since=args.since,
|
|
253
|
+
verbose=args.verbose
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
findings = analyzer.run()
|
|
257
|
+
|
|
258
|
+
if args.output == 'json':
|
|
259
|
+
print(analyzer.to_json())
|
|
260
|
+
else:
|
|
261
|
+
analyzer.print_report()
|
|
262
|
+
|
|
263
|
+
# Exit with error code if HIGH severity findings
|
|
264
|
+
high_count = len([f for f in findings if f.get('severity') == 'HIGH'])
|
|
265
|
+
sys.exit(1 if high_count > 0 else 0)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == '__main__':
|
|
269
|
+
main()
|