apigraveyard 1.0.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/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/ROADMAP_ISSUES.md +169 -0
- package/LICENSE +21 -0
- package/README.md +501 -0
- package/bin/apigraveyard.js +686 -0
- package/hooks/pre-commit +203 -0
- package/package.json +34 -0
- package/scripts/install-hooks.js +182 -0
- package/src/database.js +518 -0
- package/src/display.js +534 -0
- package/src/scanner.js +294 -0
- package/src/tester.js +578 -0
package/hooks/pre-commit
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# APIgraveyard Pre-commit Hook
|
|
4
|
+
# Prevents committing files containing exposed API keys
|
|
5
|
+
#
|
|
6
|
+
# Installation:
|
|
7
|
+
# cp hooks/pre-commit .git/hooks/pre-commit
|
|
8
|
+
# chmod +x .git/hooks/pre-commit
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
# Colors for output
|
|
12
|
+
RED='\033[0;31m'
|
|
13
|
+
YELLOW='\033[0;33m'
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
CYAN='\033[0;36m'
|
|
16
|
+
DIM='\033[2m'
|
|
17
|
+
BOLD='\033[1m'
|
|
18
|
+
NC='\033[0m' # No Color
|
|
19
|
+
|
|
20
|
+
# API Key patterns (same as scanner.js)
|
|
21
|
+
PATTERNS=(
|
|
22
|
+
# OpenAI
|
|
23
|
+
'sk-[A-Za-z0-9]{48}'
|
|
24
|
+
# Groq
|
|
25
|
+
'gsk_[A-Za-z0-9]{52}'
|
|
26
|
+
# GitHub
|
|
27
|
+
'gh[ps]_[A-Za-z0-9]{36}'
|
|
28
|
+
# Stripe
|
|
29
|
+
'sk_(live|test)_[A-Za-z0-9]{24}'
|
|
30
|
+
# Google/Firebase
|
|
31
|
+
'AIza[A-Za-z0-9_-]{35}'
|
|
32
|
+
# AWS
|
|
33
|
+
'AKIA[A-Z0-9]{16}'
|
|
34
|
+
# Anthropic
|
|
35
|
+
'sk-ant-[A-Za-z0-9_-]{95}'
|
|
36
|
+
# Hugging Face
|
|
37
|
+
'hf_[A-Za-z0-9]{34}'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Service names corresponding to patterns
|
|
41
|
+
SERVICE_NAMES=(
|
|
42
|
+
"OpenAI"
|
|
43
|
+
"Groq"
|
|
44
|
+
"GitHub"
|
|
45
|
+
"Stripe"
|
|
46
|
+
"Google/Firebase"
|
|
47
|
+
"AWS"
|
|
48
|
+
"Anthropic"
|
|
49
|
+
"Hugging Face"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Dangerous file patterns
|
|
53
|
+
DANGEROUS_FILES=(
|
|
54
|
+
'.env'
|
|
55
|
+
'.env.local'
|
|
56
|
+
'.env.production'
|
|
57
|
+
'.env.development'
|
|
58
|
+
'*.pem'
|
|
59
|
+
'*.key'
|
|
60
|
+
'*secret*'
|
|
61
|
+
'*credentials*'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Track if we found any issues
|
|
65
|
+
FOUND_KEYS=0
|
|
66
|
+
FOUND_DANGEROUS=0
|
|
67
|
+
BLOCKED_FILES=""
|
|
68
|
+
|
|
69
|
+
# Function to mask a key for display
|
|
70
|
+
mask_key() {
|
|
71
|
+
local key="$1"
|
|
72
|
+
local len=${#key}
|
|
73
|
+
if [ $len -le 8 ]; then
|
|
74
|
+
echo "****"
|
|
75
|
+
else
|
|
76
|
+
echo "${key:0:4}***...***${key: -4}"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Function to get service name for a pattern index
|
|
81
|
+
get_service_name() {
|
|
82
|
+
echo "${SERVICE_NAMES[$1]}"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Print banner
|
|
86
|
+
echo -e "${DIM}🪦 APIgraveyard: Scanning staged files for API keys...${NC}"
|
|
87
|
+
echo ""
|
|
88
|
+
|
|
89
|
+
# Get list of staged files (Added, Copied, Modified)
|
|
90
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null)
|
|
91
|
+
|
|
92
|
+
if [ -z "$STAGED_FILES" ]; then
|
|
93
|
+
echo -e "${GREEN}✓ No staged files to check.${NC}"
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Check for dangerous files being committed
|
|
98
|
+
for file in $STAGED_FILES; do
|
|
99
|
+
filename=$(basename "$file")
|
|
100
|
+
|
|
101
|
+
# Check against dangerous patterns
|
|
102
|
+
for pattern in "${DANGEROUS_FILES[@]}"; do
|
|
103
|
+
case "$filename" in
|
|
104
|
+
$pattern)
|
|
105
|
+
if [ $FOUND_DANGEROUS -eq 0 ]; then
|
|
106
|
+
echo -e "${YELLOW}⚠️ WARNING: Potentially sensitive files staged${NC}"
|
|
107
|
+
echo ""
|
|
108
|
+
fi
|
|
109
|
+
echo -e " ${YELLOW}⚠️ $file${NC}"
|
|
110
|
+
FOUND_DANGEROUS=1
|
|
111
|
+
;;
|
|
112
|
+
esac
|
|
113
|
+
done
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
if [ $FOUND_DANGEROUS -eq 1 ]; then
|
|
117
|
+
echo ""
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Scan each staged file for API keys
|
|
121
|
+
for file in $STAGED_FILES; do
|
|
122
|
+
# Skip binary files and non-existent files
|
|
123
|
+
if [ ! -f "$file" ]; then
|
|
124
|
+
continue
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Skip files that are likely binary
|
|
128
|
+
if file "$file" | grep -q "binary"; then
|
|
129
|
+
continue
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Read file content from the staged version
|
|
133
|
+
content=$(git show :"$file" 2>/dev/null)
|
|
134
|
+
|
|
135
|
+
if [ -z "$content" ]; then
|
|
136
|
+
continue
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Check each pattern
|
|
140
|
+
for i in "${!PATTERNS[@]}"; do
|
|
141
|
+
pattern="${PATTERNS[$i]}"
|
|
142
|
+
service=$(get_service_name $i)
|
|
143
|
+
|
|
144
|
+
# Find matches with line numbers
|
|
145
|
+
line_num=0
|
|
146
|
+
while IFS= read -r line; do
|
|
147
|
+
line_num=$((line_num + 1))
|
|
148
|
+
|
|
149
|
+
# Check if line matches pattern
|
|
150
|
+
if echo "$line" | grep -qE "$pattern"; then
|
|
151
|
+
# Extract the matched key
|
|
152
|
+
matched_key=$(echo "$line" | grep -oE "$pattern" | head -1)
|
|
153
|
+
masked=$(mask_key "$matched_key")
|
|
154
|
+
|
|
155
|
+
if [ $FOUND_KEYS -eq 0 ]; then
|
|
156
|
+
echo -e "${RED}${BOLD}❌ COMMIT BLOCKED - API Keys Detected!${NC}"
|
|
157
|
+
echo ""
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
echo -e "${RED}Found in:${NC} ${CYAN}$file${NC} ${DIM}(line $line_num)${NC}"
|
|
161
|
+
echo -e " ${YELLOW}$service key:${NC} $masked"
|
|
162
|
+
echo ""
|
|
163
|
+
|
|
164
|
+
FOUND_KEYS=1
|
|
165
|
+
BLOCKED_FILES="$BLOCKED_FILES $file"
|
|
166
|
+
fi
|
|
167
|
+
done <<< "$content"
|
|
168
|
+
done
|
|
169
|
+
done
|
|
170
|
+
|
|
171
|
+
# If keys were found, show suggestions and exit with error
|
|
172
|
+
if [ $FOUND_KEYS -eq 1 ]; then
|
|
173
|
+
echo -e "${DIM}─────────────────────────────────────────────────${NC}"
|
|
174
|
+
echo ""
|
|
175
|
+
echo -e "${BOLD}Suggested fixes:${NC}"
|
|
176
|
+
echo -e " ${GREEN}1.${NC} Move keys to ${CYAN}.env${NC} file"
|
|
177
|
+
echo -e " ${GREEN}2.${NC} Add ${CYAN}.env${NC} to ${CYAN}.gitignore${NC}"
|
|
178
|
+
echo -e " ${GREEN}3.${NC} Unstage the file(s):"
|
|
179
|
+
|
|
180
|
+
# Show unique files
|
|
181
|
+
unique_files=$(echo "$BLOCKED_FILES" | tr ' ' '\n' | sort -u)
|
|
182
|
+
for f in $unique_files; do
|
|
183
|
+
if [ -n "$f" ]; then
|
|
184
|
+
echo -e " ${DIM}git reset HEAD $f${NC}"
|
|
185
|
+
fi
|
|
186
|
+
done
|
|
187
|
+
|
|
188
|
+
echo ""
|
|
189
|
+
echo -e "${DIM}To bypass this check (NOT recommended):${NC}"
|
|
190
|
+
echo -e " ${DIM}git commit --no-verify${NC}"
|
|
191
|
+
echo ""
|
|
192
|
+
|
|
193
|
+
exit 1
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# Check if only dangerous files warning (not blocking)
|
|
197
|
+
if [ $FOUND_DANGEROUS -eq 1 ]; then
|
|
198
|
+
echo -e "${YELLOW}Consider adding sensitive files to .gitignore${NC}"
|
|
199
|
+
echo ""
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
echo -e "${GREEN}✓ No API keys detected in staged files.${NC}"
|
|
203
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apigraveyard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI tool to detect and manage deprecated APIs in your codebase",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"apigraveyard": "./bin/apigraveyard.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/apigraveyard.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
13
|
+
"postinstall": "node scripts/install-hooks.js",
|
|
14
|
+
"install-hooks": "node scripts/install-hooks.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"api",
|
|
18
|
+
"deprecated",
|
|
19
|
+
"cli",
|
|
20
|
+
"scanner",
|
|
21
|
+
"graveyard"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"commander": "^11.0.0",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"axios": "^1.6.0",
|
|
29
|
+
"ora": "^7.0.0",
|
|
30
|
+
"cli-table3": "^0.6.3",
|
|
31
|
+
"glob": "^10.3.0",
|
|
32
|
+
"dotenv": "^16.3.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* APIgraveyard Hook Installer
|
|
5
|
+
* Installs git hooks for API key detection
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npm run install-hooks
|
|
9
|
+
* node scripts/install-hooks.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
|
|
20
|
+
// Colors for console output
|
|
21
|
+
const colors = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
red: '\x1b[31m',
|
|
24
|
+
green: '\x1b[32m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
cyan: '\x1b[36m',
|
|
27
|
+
dim: '\x1b[2m'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Logs a message with color
|
|
32
|
+
*/
|
|
33
|
+
function log(message, color = 'reset') {
|
|
34
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Finds the git root directory
|
|
39
|
+
*/
|
|
40
|
+
function findGitRoot() {
|
|
41
|
+
try {
|
|
42
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', {
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
45
|
+
}).trim();
|
|
46
|
+
return gitRoot;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Main installation function
|
|
54
|
+
*/
|
|
55
|
+
function installHooks() {
|
|
56
|
+
log('\n🪦 APIgraveyard Hook Installer\n', 'cyan');
|
|
57
|
+
|
|
58
|
+
// Find git root
|
|
59
|
+
const gitRoot = findGitRoot();
|
|
60
|
+
|
|
61
|
+
if (!gitRoot) {
|
|
62
|
+
log('⚠️ Not a git repository. Skipping hook installation.', 'yellow');
|
|
63
|
+
log(' Run "git init" first, then "npm run install-hooks"\n', 'dim');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hooksDir = path.join(gitRoot, '.git', 'hooks');
|
|
68
|
+
const sourceHook = path.join(__dirname, '..', 'hooks', 'pre-commit');
|
|
69
|
+
const targetHook = path.join(hooksDir, 'pre-commit');
|
|
70
|
+
|
|
71
|
+
// Check if source hook exists
|
|
72
|
+
if (!fs.existsSync(sourceHook)) {
|
|
73
|
+
log('❌ Source hook not found: ' + sourceHook, 'red');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Ensure hooks directory exists
|
|
78
|
+
if (!fs.existsSync(hooksDir)) {
|
|
79
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
80
|
+
log('📁 Created hooks directory', 'dim');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if hook already exists
|
|
84
|
+
if (fs.existsSync(targetHook)) {
|
|
85
|
+
// Read existing hook to check if it's ours
|
|
86
|
+
const existingContent = fs.readFileSync(targetHook, 'utf-8');
|
|
87
|
+
|
|
88
|
+
if (existingContent.includes('APIgraveyard')) {
|
|
89
|
+
log('✓ Pre-commit hook already installed', 'green');
|
|
90
|
+
|
|
91
|
+
// Update it anyway in case of changes
|
|
92
|
+
const sourceContent = fs.readFileSync(sourceHook, 'utf-8');
|
|
93
|
+
fs.writeFileSync(targetHook, sourceContent, { mode: 0o755 });
|
|
94
|
+
log(' Updated to latest version', 'dim');
|
|
95
|
+
} else {
|
|
96
|
+
// There's an existing hook that's not ours
|
|
97
|
+
log('⚠️ Existing pre-commit hook found', 'yellow');
|
|
98
|
+
log(' Backing up to pre-commit.backup', 'dim');
|
|
99
|
+
|
|
100
|
+
// Backup existing hook
|
|
101
|
+
fs.copyFileSync(targetHook, targetHook + '.backup');
|
|
102
|
+
|
|
103
|
+
// Install our hook
|
|
104
|
+
const sourceContent = fs.readFileSync(sourceHook, 'utf-8');
|
|
105
|
+
fs.writeFileSync(targetHook, sourceContent, { mode: 0o755 });
|
|
106
|
+
log('✓ Pre-commit hook installed', 'green');
|
|
107
|
+
log(' Your old hook was saved to pre-commit.backup', 'dim');
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// No existing hook, just copy
|
|
111
|
+
const sourceContent = fs.readFileSync(sourceHook, 'utf-8');
|
|
112
|
+
fs.writeFileSync(targetHook, sourceContent, { mode: 0o755 });
|
|
113
|
+
log('✓ Pre-commit hook installed', 'green');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Make executable on Unix systems
|
|
117
|
+
try {
|
|
118
|
+
if (process.platform !== 'win32') {
|
|
119
|
+
fs.chmodSync(targetHook, 0o755);
|
|
120
|
+
log(' Made hook executable', 'dim');
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
log('⚠️ Could not set executable permission: ' + err.message, 'yellow');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log('\n📍 Hook installed at: ' + targetHook, 'dim');
|
|
127
|
+
log('\n✅ APIgraveyard will now scan for API keys before each commit!\n', 'green');
|
|
128
|
+
|
|
129
|
+
// Show how to bypass
|
|
130
|
+
log('To bypass the check (not recommended):', 'dim');
|
|
131
|
+
log(' git commit --no-verify\n', 'dim');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Uninstall function
|
|
136
|
+
*/
|
|
137
|
+
function uninstallHooks() {
|
|
138
|
+
log('\n🪦 APIgraveyard Hook Uninstaller\n', 'cyan');
|
|
139
|
+
|
|
140
|
+
const gitRoot = findGitRoot();
|
|
141
|
+
|
|
142
|
+
if (!gitRoot) {
|
|
143
|
+
log('⚠️ Not a git repository.', 'yellow');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const targetHook = path.join(gitRoot, '.git', 'hooks', 'pre-commit');
|
|
148
|
+
const backupHook = targetHook + '.backup';
|
|
149
|
+
|
|
150
|
+
if (!fs.existsSync(targetHook)) {
|
|
151
|
+
log('ℹ️ No pre-commit hook installed', 'dim');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const content = fs.readFileSync(targetHook, 'utf-8');
|
|
156
|
+
|
|
157
|
+
if (!content.includes('APIgraveyard')) {
|
|
158
|
+
log('⚠️ Pre-commit hook is not from APIgraveyard', 'yellow');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Remove our hook
|
|
163
|
+
fs.unlinkSync(targetHook);
|
|
164
|
+
log('✓ Pre-commit hook removed', 'green');
|
|
165
|
+
|
|
166
|
+
// Restore backup if exists
|
|
167
|
+
if (fs.existsSync(backupHook)) {
|
|
168
|
+
fs.renameSync(backupHook, targetHook);
|
|
169
|
+
log(' Restored previous hook from backup', 'dim');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
log('\n✅ APIgraveyard hooks uninstalled\n', 'green');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check command line arguments
|
|
176
|
+
const args = process.argv.slice(2);
|
|
177
|
+
|
|
178
|
+
if (args.includes('--uninstall') || args.includes('-u')) {
|
|
179
|
+
uninstallHooks();
|
|
180
|
+
} else {
|
|
181
|
+
installHooks();
|
|
182
|
+
}
|