@telelabsai/ship 1.1.3 → 1.1.6
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/.claude/agents/reviewer.md +47 -0
- package/.claude/hooks/git-safety.cjs +14 -22
- package/.claude/rules/code-quality.md +15 -0
- package/.claude/rules/security.md +10 -0
- package/.claude/shared/secret-patterns.json +62 -0
- package/.claude/skills/ship-review/SKILL.md +74 -0
- package/.claude/skills/ship-review/references/output-format.md +62 -0
- package/.claude/skills/ship-review/references/review-categories.md +169 -0
- package/.claude/skills/ship-review/references/severity-levels.md +49 -0
- package/.claude/skills/ship-secure/SKILL.md +60 -0
- package/.claude/skills/ship-secure/references/dependency-audit.md +71 -0
- package/.claude/skills/ship-secure/references/owasp-checks.md +197 -0
- package/.claude/skills/ship-secure/references/secret-patterns.md +50 -0
- package/.claude/skills/ship-test/SKILL.md +85 -0
- package/.claude/skills/ship-test/references/coverage-analysis.md +70 -0
- package/.claude/skills/ship-test/references/output-format.md +71 -0
- package/.claude/skills/ship-test/references/test-workflow.md +47 -0
- package/.claude/skills/ship-version/SKILL.md +4 -0
- package/.claude/skills/ship-version/references/safety-protocols.md +10 -16
- package/README.md +102 -11
- package/cli/bin.js +18 -1
- package/cli/commands/auth.js +141 -0
- package/cli/commands/init.js +37 -6
- package/cli/commands/sync.js +191 -0
- package/git-hooks/pre-commit.cjs +143 -0
- package/package.json +2 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Git pre-commit hook — scans staged file CONTENTS for secrets.
|
|
5
|
+
* Reads patterns from .claude/shared/secret-patterns.json (single source of truth).
|
|
6
|
+
*
|
|
7
|
+
* Exit 0 = allow commit
|
|
8
|
+
* Exit 1 = block commit (secrets found)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Load patterns from shared config
|
|
16
|
+
const patternsPath = path.join(__dirname, '..', '.claude', 'shared', 'secret-patterns.json');
|
|
17
|
+
const patterns = JSON.parse(fs.readFileSync(patternsPath, 'utf8'));
|
|
18
|
+
|
|
19
|
+
// Get staged files
|
|
20
|
+
const stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' })
|
|
21
|
+
.trim()
|
|
22
|
+
.split('\n')
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
|
|
25
|
+
if (stagedFiles.length === 0) {
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Build regex patterns from all categories
|
|
30
|
+
const regexPatterns = [];
|
|
31
|
+
for (const category of ['apiKeys', 'credentials', 'databaseUrls', 'privateKeys']) {
|
|
32
|
+
for (const entry of patterns[category]) {
|
|
33
|
+
regexPatterns.push({
|
|
34
|
+
name: entry.name,
|
|
35
|
+
regex: new RegExp(entry.pattern, 'g'),
|
|
36
|
+
severity: entry.severity,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build false positive checker
|
|
42
|
+
const falsePositives = patterns.falsePositives;
|
|
43
|
+
function isFalsePositive(line) {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*')) {
|
|
46
|
+
return false; // Don't skip comments — secrets in comments are still secrets
|
|
47
|
+
}
|
|
48
|
+
return falsePositives.some((fp) => line.includes(fp));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Skip binary and irrelevant files
|
|
52
|
+
const skipExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.mp4', '.mp3', '.zip', '.tar', '.gz'];
|
|
53
|
+
const skipFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
|
|
54
|
+
|
|
55
|
+
// Check dangerous files being committed
|
|
56
|
+
const findings = [];
|
|
57
|
+
|
|
58
|
+
for (const file of stagedFiles) {
|
|
59
|
+
// Check if file itself is dangerous
|
|
60
|
+
for (const dangerousFile of patterns.dangerousFiles) {
|
|
61
|
+
const pattern = dangerousFile.replace(/\*/g, '.*');
|
|
62
|
+
if (new RegExp(`(^|/)${pattern}$`).test(file)) {
|
|
63
|
+
findings.push({
|
|
64
|
+
file,
|
|
65
|
+
line: 0,
|
|
66
|
+
name: `Sensitive file: ${dangerousFile}`,
|
|
67
|
+
severity: 'critical',
|
|
68
|
+
match: file,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Skip binary files
|
|
74
|
+
const ext = path.extname(file).toLowerCase();
|
|
75
|
+
if (skipExtensions.includes(ext)) continue;
|
|
76
|
+
if (skipFiles.includes(path.basename(file))) continue;
|
|
77
|
+
|
|
78
|
+
// Read staged content
|
|
79
|
+
let content;
|
|
80
|
+
try {
|
|
81
|
+
content = execSync(`git show :${file}`, { encoding: 'utf8' });
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Scan each line
|
|
87
|
+
const lines = content.split('\n');
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
const line = lines[i];
|
|
90
|
+
|
|
91
|
+
if (isFalsePositive(line)) continue;
|
|
92
|
+
|
|
93
|
+
for (const { name, regex, severity } of regexPatterns) {
|
|
94
|
+
regex.lastIndex = 0;
|
|
95
|
+
if (regex.test(line)) {
|
|
96
|
+
findings.push({
|
|
97
|
+
file,
|
|
98
|
+
line: i + 1,
|
|
99
|
+
name,
|
|
100
|
+
severity,
|
|
101
|
+
match: line.trim().substring(0, 80),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (findings.length === 0) {
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Report findings
|
|
113
|
+
console.error('\n Ship: Secrets detected in staged files!\n');
|
|
114
|
+
|
|
115
|
+
const critical = findings.filter((f) => f.severity === 'critical');
|
|
116
|
+
const high = findings.filter((f) => f.severity === 'high');
|
|
117
|
+
const medium = findings.filter((f) => f.severity === 'medium');
|
|
118
|
+
|
|
119
|
+
if (critical.length) {
|
|
120
|
+
console.error(' Critical:');
|
|
121
|
+
for (const f of critical) {
|
|
122
|
+
console.error(` ${f.file}:${f.line} — ${f.name}`);
|
|
123
|
+
if (f.match && f.line > 0) console.error(` ${f.match}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (high.length) {
|
|
128
|
+
console.error('\n High:');
|
|
129
|
+
for (const f of high) {
|
|
130
|
+
console.error(` ${f.file}:${f.line} — ${f.name}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (medium.length) {
|
|
135
|
+
console.error('\n Medium:');
|
|
136
|
+
for (const f of medium) {
|
|
137
|
+
console.error(` ${f.file}:${f.line} — ${f.name}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.error(`\n ${findings.length} secret(s) found. Commit blocked.`);
|
|
142
|
+
console.error(' Fix: Move secrets to environment variables or add files to .gitignore.\n');
|
|
143
|
+
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telelabsai/ship",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Ship code faster with pre-configured agents, skills, rules, and hooks for Claude Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ship": "cli/bin.js"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"cli/",
|
|
10
10
|
".claude/",
|
|
11
|
+
"git-hooks/",
|
|
11
12
|
"CLAUDE.md",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|