tlc-claude-code 1.6.4 → 1.7.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/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
- package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
- package/package.json +1 -1
- package/server/lib/access-control.test.js +1 -1
- package/server/lib/agents-cancel-command.test.js +1 -1
- package/server/lib/agents-get-command.test.js +1 -1
- package/server/lib/agents-list-command.test.js +1 -1
- package/server/lib/agents-logs-command.test.js +1 -1
- package/server/lib/agents-retry-command.test.js +1 -1
- package/server/lib/budget-limits.test.js +2 -2
- package/server/lib/code-gate/bypass-logger.js +129 -0
- package/server/lib/code-gate/bypass-logger.test.js +142 -0
- package/server/lib/code-gate/gate-command.js +114 -0
- package/server/lib/code-gate/gate-command.test.js +111 -0
- package/server/lib/code-gate/gate-config.js +163 -0
- package/server/lib/code-gate/gate-config.test.js +181 -0
- package/server/lib/code-gate/gate-engine.js +193 -0
- package/server/lib/code-gate/gate-engine.test.js +258 -0
- package/server/lib/code-gate/gate-reporter.js +123 -0
- package/server/lib/code-gate/gate-reporter.test.js +159 -0
- package/server/lib/code-gate/hooks-generator.js +149 -0
- package/server/lib/code-gate/hooks-generator.test.js +142 -0
- package/server/lib/code-gate/llm-reviewer.js +176 -0
- package/server/lib/code-gate/llm-reviewer.test.js +161 -0
- package/server/lib/code-gate/push-gate.js +133 -0
- package/server/lib/code-gate/push-gate.test.js +190 -0
- package/server/lib/code-gate/rules/architecture-rules.js +228 -0
- package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
- package/server/lib/code-gate/rules/client-rules.js +120 -0
- package/server/lib/code-gate/rules/client-rules.test.js +121 -0
- package/server/lib/code-gate/rules/config-rules.js +140 -0
- package/server/lib/code-gate/rules/config-rules.test.js +103 -0
- package/server/lib/code-gate/rules/database-rules.js +158 -0
- package/server/lib/code-gate/rules/database-rules.test.js +119 -0
- package/server/lib/code-gate/rules/docker-rules.js +201 -0
- package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
- package/server/lib/code-gate/rules/quality-rules.js +304 -0
- package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
- package/server/lib/code-gate/rules/security-rules.js +228 -0
- package/server/lib/code-gate/rules/security-rules.test.js +131 -0
- package/server/lib/code-gate/rules/structure-rules.js +155 -0
- package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
- package/server/lib/code-gate/rules/test-rules.js +93 -0
- package/server/lib/code-gate/rules/test-rules.test.js +97 -0
- package/server/lib/code-gate/typescript-gate.js +128 -0
- package/server/lib/code-gate/typescript-gate.test.js +131 -0
- package/server/lib/code-generator.test.js +1 -1
- package/server/lib/cost-command.test.js +1 -1
- package/server/lib/cost-optimizer.test.js +1 -1
- package/server/lib/cost-projections.test.js +1 -1
- package/server/lib/cost-reports.test.js +1 -1
- package/server/lib/cost-tracker.test.js +1 -1
- package/server/lib/crypto-patterns.test.js +1 -1
- package/server/lib/design-command.test.js +1 -1
- package/server/lib/design-parser.test.js +1 -1
- package/server/lib/gemini-vision.test.js +1 -1
- package/server/lib/input-validator.test.js +1 -1
- package/server/lib/litellm-client.test.js +1 -1
- package/server/lib/litellm-command.test.js +1 -1
- package/server/lib/litellm-config.test.js +1 -1
- package/server/lib/model-pricing.test.js +1 -1
- package/server/lib/models-command.test.js +1 -1
- package/server/lib/optimize-command.test.js +1 -1
- package/server/lib/orchestration-integration.test.js +1 -1
- package/server/lib/output-encoder.test.js +1 -1
- package/server/lib/quality-evaluator.test.js +1 -1
- package/server/lib/quality-gate-command.test.js +1 -1
- package/server/lib/quality-gate-scorer.test.js +1 -1
- package/server/lib/quality-history.test.js +1 -1
- package/server/lib/quality-presets.test.js +1 -1
- package/server/lib/quality-retry.test.js +1 -1
- package/server/lib/quality-thresholds.test.js +1 -1
- package/server/lib/secure-auth.test.js +1 -1
- package/server/lib/secure-code-command.test.js +1 -1
- package/server/lib/secure-errors.test.js +1 -1
- package/server/lib/security/auth-security.test.js +4 -3
- package/server/lib/vision-command.test.js +1 -1
- package/server/lib/visual-command.test.js +1 -1
- package/server/lib/visual-testing.test.js +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Rules
|
|
3
|
+
*
|
|
4
|
+
* Detects new Date() in ORM .set() blocks (should use sql`now()`)
|
|
5
|
+
* and inline billing math that should use shared calculation utilities.
|
|
6
|
+
*
|
|
7
|
+
* Derived from production bugs: timestamp drift, copy-paste billing errors.
|
|
8
|
+
* See: WALL_OF_SHAME.md Bug #29 (missing VAT), Lesson #3 (timestamps)
|
|
9
|
+
*
|
|
10
|
+
* @module code-gate/rules/database-rules
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} filePath
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isTestFile(filePath) {
|
|
18
|
+
return /\.(test|spec)\.[jt]sx?$/.test(filePath) || filePath.includes('__tests__');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect new Date() inside ORM .set({}) blocks.
|
|
23
|
+
* Database timestamps should use sql`now()` for consistent DB-server time.
|
|
24
|
+
* new Date() uses app-server time which can drift.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} filePath
|
|
27
|
+
* @param {string} content
|
|
28
|
+
* @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
|
|
29
|
+
*/
|
|
30
|
+
function checkNewDateInSet(filePath, content) {
|
|
31
|
+
if (isTestFile(filePath)) return [];
|
|
32
|
+
const findings = [];
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
|
|
35
|
+
let inSetBlock = false;
|
|
36
|
+
let braceDepth = 0;
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i];
|
|
40
|
+
|
|
41
|
+
// Detect .set({ or .set( { start
|
|
42
|
+
if (/\.set\s*\(\s*\{/.test(line)) {
|
|
43
|
+
inSetBlock = true;
|
|
44
|
+
braceDepth = 0;
|
|
45
|
+
// Count braces on this line
|
|
46
|
+
for (const ch of line) {
|
|
47
|
+
if (ch === '{') braceDepth++;
|
|
48
|
+
if (ch === '}') braceDepth--;
|
|
49
|
+
}
|
|
50
|
+
if (braceDepth <= 0) {
|
|
51
|
+
// Single-line .set({...}) — check this line
|
|
52
|
+
if (/new\s+Date\s*\(\s*\)/.test(line)) {
|
|
53
|
+
findings.push({
|
|
54
|
+
severity: 'block',
|
|
55
|
+
rule: 'no-new-date-in-set',
|
|
56
|
+
line: i + 1,
|
|
57
|
+
message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
|
|
58
|
+
fix: 'Replace new Date() with sql`now()` for database-server time consistency',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
inSetBlock = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Check the opening line too
|
|
65
|
+
if (/new\s+Date\s*\(\s*\)/.test(line)) {
|
|
66
|
+
findings.push({
|
|
67
|
+
severity: 'block',
|
|
68
|
+
rule: 'no-new-date-in-set',
|
|
69
|
+
line: i + 1,
|
|
70
|
+
message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
|
|
71
|
+
fix: 'Replace new Date() with sql`now()` for database-server time consistency',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (inSetBlock) {
|
|
78
|
+
for (const ch of line) {
|
|
79
|
+
if (ch === '{') braceDepth++;
|
|
80
|
+
if (ch === '}') braceDepth--;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (/new\s+Date\s*\(\s*\)/.test(line)) {
|
|
84
|
+
findings.push({
|
|
85
|
+
severity: 'block',
|
|
86
|
+
rule: 'no-new-date-in-set',
|
|
87
|
+
line: i + 1,
|
|
88
|
+
message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
|
|
89
|
+
fix: 'Replace new Date() with sql`now()` for database-server time consistency',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (braceDepth <= 0) {
|
|
94
|
+
inSetBlock = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return findings;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Billing-related variable names that indicate inline math */
|
|
103
|
+
const BILLING_VARS = [
|
|
104
|
+
'quantity', 'rate', 'price', 'cost', 'amount',
|
|
105
|
+
'subtotal', 'discount', 'tax', 'vat', 'total',
|
|
106
|
+
'lineTotal', 'grandTotal', 'unitPrice',
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Detect inline billing math that should use shared calculation utilities.
|
|
111
|
+
* When billing calculations are scattered across files, bugs like missing VAT
|
|
112
|
+
* or wrong discount logic slip through.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} filePath
|
|
115
|
+
* @param {string} content
|
|
116
|
+
* @returns {Array}
|
|
117
|
+
*/
|
|
118
|
+
function checkInlineBillingMath(filePath, content) {
|
|
119
|
+
if (isTestFile(filePath)) return [];
|
|
120
|
+
|
|
121
|
+
// Allow math in calculation utility files
|
|
122
|
+
const fileBase = filePath.toLowerCase();
|
|
123
|
+
if (fileBase.includes('calculation') || fileBase.includes('calc.') ||
|
|
124
|
+
fileBase.includes('calc/') || fileBase.includes('-calc')) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const findings = [];
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
const line = lines[i];
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
|
135
|
+
|
|
136
|
+
// Check for billing-variable arithmetic: quantity * rate, subtotal - discount, etc.
|
|
137
|
+
for (const varName of BILLING_VARS) {
|
|
138
|
+
const pattern = new RegExp(`\\b${varName}\\b\\s*[*+\\-]\\s*\\b(${BILLING_VARS.join('|')})\\b`);
|
|
139
|
+
if (pattern.test(line)) {
|
|
140
|
+
findings.push({
|
|
141
|
+
severity: 'warn',
|
|
142
|
+
rule: 'no-inline-billing-math',
|
|
143
|
+
line: i + 1,
|
|
144
|
+
message: 'Inline billing calculation — use shared calculation utility',
|
|
145
|
+
fix: 'Use calculateLineTotal(), calculateSubtotal(), or calculateGrandTotal() from billing-calculations module',
|
|
146
|
+
});
|
|
147
|
+
break; // One finding per line
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return findings;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
checkNewDateInSet,
|
|
157
|
+
checkInlineBillingMath,
|
|
158
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Rules Tests
|
|
3
|
+
*
|
|
4
|
+
* Detects new Date() in ORM .set() blocks and inline billing math.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
checkNewDateInSet,
|
|
10
|
+
checkInlineBillingMath,
|
|
11
|
+
} = require('./database-rules.js');
|
|
12
|
+
|
|
13
|
+
describe('Database Rules', () => {
|
|
14
|
+
describe('checkNewDateInSet', () => {
|
|
15
|
+
it('detects new Date() in .set({}) block', () => {
|
|
16
|
+
const code = `
|
|
17
|
+
db.update(leads).set({
|
|
18
|
+
status: "active",
|
|
19
|
+
updatedAt: new Date(),
|
|
20
|
+
});
|
|
21
|
+
`;
|
|
22
|
+
const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
|
|
23
|
+
expect(findings).toHaveLength(1);
|
|
24
|
+
expect(findings[0].severity).toBe('block');
|
|
25
|
+
expect(findings[0].rule).toBe('no-new-date-in-set');
|
|
26
|
+
expect(findings[0].fix).toContain('sql');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('detects new Date() in .set() with convertedAt', () => {
|
|
30
|
+
const code = `
|
|
31
|
+
db.update(leads).set({
|
|
32
|
+
convertedAt: new Date(),
|
|
33
|
+
updatedAt: new Date(),
|
|
34
|
+
});
|
|
35
|
+
`;
|
|
36
|
+
const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
|
|
37
|
+
expect(findings.length).toBeGreaterThanOrEqual(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('allows new Date() in logging context', () => {
|
|
41
|
+
const code = `
|
|
42
|
+
console.log('Started at', new Date());
|
|
43
|
+
const timestamp = new Date();
|
|
44
|
+
`;
|
|
45
|
+
const findings = checkNewDateInSet('src/utils/logger.ts', code);
|
|
46
|
+
expect(findings).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('allows sql`now()` in .set()', () => {
|
|
50
|
+
const code = `
|
|
51
|
+
db.update(leads).set({
|
|
52
|
+
status: "active",
|
|
53
|
+
updatedAt: sql\`now()\`,
|
|
54
|
+
});
|
|
55
|
+
`;
|
|
56
|
+
const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
|
|
57
|
+
expect(findings).toHaveLength(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('skips test files', () => {
|
|
61
|
+
const code = 'db.update(leads).set({ updatedAt: new Date() });';
|
|
62
|
+
const findings = checkNewDateInSet('src/leads/leads.test.ts', code);
|
|
63
|
+
expect(findings).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('detects across multiline .set() call', () => {
|
|
67
|
+
const code = [
|
|
68
|
+
'await db',
|
|
69
|
+
' .update(invoices)',
|
|
70
|
+
' .set({',
|
|
71
|
+
' paidAt: new Date(),',
|
|
72
|
+
' status: "paid",',
|
|
73
|
+
' });',
|
|
74
|
+
].join('\n');
|
|
75
|
+
const findings = checkNewDateInSet('src/invoices/invoices.service.ts', code);
|
|
76
|
+
expect(findings).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('checkInlineBillingMath', () => {
|
|
81
|
+
it('detects quantity * rate pattern', () => {
|
|
82
|
+
const code = 'const lineTotal = quantity * rate;';
|
|
83
|
+
const findings = checkInlineBillingMath('src/invoices/editor.tsx', code);
|
|
84
|
+
expect(findings).toHaveLength(1);
|
|
85
|
+
expect(findings[0].severity).toBe('warn');
|
|
86
|
+
expect(findings[0].rule).toBe('no-inline-billing-math');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('detects subtotal - discount pattern', () => {
|
|
90
|
+
const code = 'const total = subtotal - discount + tax;';
|
|
91
|
+
const findings = checkInlineBillingMath('src/invoices/sheet.tsx', code);
|
|
92
|
+
expect(findings).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('allows math in calculation utility files', () => {
|
|
96
|
+
const code = 'const lineTotal = quantity * rate;';
|
|
97
|
+
const findings = checkInlineBillingMath('src/lib/billing-calculations.ts', code);
|
|
98
|
+
expect(findings).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('allows math in files with "calc" in name', () => {
|
|
102
|
+
const code = 'const total = subtotal - discount;';
|
|
103
|
+
const findings = checkInlineBillingMath('src/utils/price-calc.ts', code);
|
|
104
|
+
expect(findings).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('skips test files', () => {
|
|
108
|
+
const code = 'const lineTotal = quantity * rate;';
|
|
109
|
+
const findings = checkInlineBillingMath('src/invoices/editor.test.tsx', code);
|
|
110
|
+
expect(findings).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('allows non-billing arithmetic', () => {
|
|
114
|
+
const code = 'const area = width * height;';
|
|
115
|
+
const findings = checkInlineBillingMath('src/utils/geometry.ts', code);
|
|
116
|
+
expect(findings).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Rules
|
|
3
|
+
*
|
|
4
|
+
* Detects dangerous Docker patterns that can cause data loss:
|
|
5
|
+
* external volumes, missing volume names, destructive commands.
|
|
6
|
+
*
|
|
7
|
+
* Derived from Bug #27 (AI wiped production database via docker volume).
|
|
8
|
+
* See: TLC-BEST-PRACTICES.md Section 7 (Docker & Infrastructure)
|
|
9
|
+
*
|
|
10
|
+
* @module code-gate/rules/docker-rules
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* File types where docker-compose rules apply.
|
|
15
|
+
* @param {string} filePath
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
function isDockerComposeFile(filePath) {
|
|
19
|
+
const base = filePath.toLowerCase();
|
|
20
|
+
return base.includes('docker-compose') || base.includes('compose.y');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* File types where shell command rules apply.
|
|
25
|
+
* @param {string} filePath
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
function isShellOrCIFile(filePath) {
|
|
29
|
+
const base = filePath.toLowerCase();
|
|
30
|
+
return base.endsWith('.sh') || base.endsWith('.bash') ||
|
|
31
|
+
base.includes('.github/') || base.includes('.gitlab-ci') ||
|
|
32
|
+
base.includes('makefile') || base.includes('Makefile') ||
|
|
33
|
+
base.endsWith('.yml') || base.endsWith('.yaml');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detect `external: true` in docker-compose volume definitions.
|
|
38
|
+
* External volumes create fragile cross-project dependencies
|
|
39
|
+
* and can silently reference wrong data.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} filePath
|
|
42
|
+
* @param {string} content
|
|
43
|
+
* @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
|
|
44
|
+
*/
|
|
45
|
+
function checkExternalVolumes(filePath, content) {
|
|
46
|
+
if (!isDockerComposeFile(filePath)) return [];
|
|
47
|
+
const findings = [];
|
|
48
|
+
const lines = content.split('\n');
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
const line = lines[i];
|
|
52
|
+
if (/^\s*external\s*:\s*true/.test(line)) {
|
|
53
|
+
findings.push({
|
|
54
|
+
severity: 'block',
|
|
55
|
+
rule: 'no-external-volumes',
|
|
56
|
+
line: i + 1,
|
|
57
|
+
message: 'Docker volume with external: true — creates fragile cross-project dependency',
|
|
58
|
+
fix: 'Remove external: true and use explicit name: property instead',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Detect docker-compose volumes without explicit `name:` property.
|
|
68
|
+
* Without explicit names, Docker generates project-prefixed names
|
|
69
|
+
* that can collide or be accidentally deleted.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} filePath
|
|
72
|
+
* @param {string} content
|
|
73
|
+
* @returns {Array}
|
|
74
|
+
*/
|
|
75
|
+
function checkMissingVolumeNames(filePath, content) {
|
|
76
|
+
if (!isDockerComposeFile(filePath)) return [];
|
|
77
|
+
const findings = [];
|
|
78
|
+
const lines = content.split('\n');
|
|
79
|
+
|
|
80
|
+
let inVolumesSection = false;
|
|
81
|
+
let currentVolume = null;
|
|
82
|
+
let currentVolumeHasName = false;
|
|
83
|
+
let currentVolumeLine = -1;
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
const line = lines[i];
|
|
87
|
+
|
|
88
|
+
// Detect top-level volumes: section
|
|
89
|
+
if (/^volumes\s*:/.test(line)) {
|
|
90
|
+
inVolumesSection = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Exit volumes section on next top-level key
|
|
95
|
+
if (inVolumesSection && /^\S/.test(line) && !/^\s/.test(line) && !line.startsWith('#')) {
|
|
96
|
+
// Flush last volume
|
|
97
|
+
if (currentVolume && !currentVolumeHasName) {
|
|
98
|
+
findings.push({
|
|
99
|
+
severity: 'warn',
|
|
100
|
+
rule: 'require-volume-names',
|
|
101
|
+
line: currentVolumeLine + 1,
|
|
102
|
+
message: `Volume '${currentVolume}' has no explicit name: — data may be lost on project rename`,
|
|
103
|
+
fix: `Add name: property: name: ${currentVolume}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
inVolumesSection = false;
|
|
107
|
+
currentVolume = null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (inVolumesSection) {
|
|
112
|
+
// Volume key (indented once, ends with colon)
|
|
113
|
+
const volumeKey = line.match(/^\s{2}(\w[\w-]*)\s*:/);
|
|
114
|
+
if (volumeKey) {
|
|
115
|
+
// Flush previous volume
|
|
116
|
+
if (currentVolume && !currentVolumeHasName) {
|
|
117
|
+
findings.push({
|
|
118
|
+
severity: 'warn',
|
|
119
|
+
rule: 'require-volume-names',
|
|
120
|
+
line: currentVolumeLine + 1,
|
|
121
|
+
message: `Volume '${currentVolume}' has no explicit name: — data may be lost on project rename`,
|
|
122
|
+
fix: `Add name: property: name: ${currentVolume}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
currentVolume = volumeKey[1];
|
|
126
|
+
currentVolumeLine = i;
|
|
127
|
+
currentVolumeHasName = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for name: property (indented further)
|
|
131
|
+
if (currentVolume && /^\s+name\s*:/.test(line)) {
|
|
132
|
+
currentVolumeHasName = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Flush final volume
|
|
138
|
+
if (inVolumesSection && currentVolume && !currentVolumeHasName) {
|
|
139
|
+
findings.push({
|
|
140
|
+
severity: 'warn',
|
|
141
|
+
rule: 'require-volume-names',
|
|
142
|
+
line: currentVolumeLine + 1,
|
|
143
|
+
message: `Volume '${currentVolume}' has no explicit name: — data may be lost on project rename`,
|
|
144
|
+
fix: `Add name: property: name: ${currentVolume}`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return findings;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Detect dangerous Docker commands in scripts and CI files.
|
|
153
|
+
* `docker compose down -v` removes data volumes.
|
|
154
|
+
* `docker volume rm` deletes volumes directly.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} filePath
|
|
157
|
+
* @param {string} content
|
|
158
|
+
* @returns {Array}
|
|
159
|
+
*/
|
|
160
|
+
function checkDangerousDockerCommands(filePath, content) {
|
|
161
|
+
if (!isShellOrCIFile(filePath)) return [];
|
|
162
|
+
const findings = [];
|
|
163
|
+
const lines = content.split('\n');
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
166
|
+
const line = lines[i];
|
|
167
|
+
const trimmed = line.trim();
|
|
168
|
+
if (trimmed.startsWith('#')) continue;
|
|
169
|
+
|
|
170
|
+
// docker compose down -v (or docker-compose down -v)
|
|
171
|
+
if (/docker[\s-]compose\s+down\s+.*-v/.test(line) ||
|
|
172
|
+
/docker[\s-]compose\s+down\s+-v/.test(line)) {
|
|
173
|
+
findings.push({
|
|
174
|
+
severity: 'block',
|
|
175
|
+
rule: 'no-dangerous-docker',
|
|
176
|
+
line: i + 1,
|
|
177
|
+
message: 'docker compose down -v removes data volumes — potential data loss',
|
|
178
|
+
fix: 'Use docker compose down (without -v) to preserve data volumes',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// docker volume rm
|
|
183
|
+
if (/docker\s+volume\s+rm\b/.test(line)) {
|
|
184
|
+
findings.push({
|
|
185
|
+
severity: 'block',
|
|
186
|
+
rule: 'no-dangerous-docker',
|
|
187
|
+
line: i + 1,
|
|
188
|
+
message: 'docker volume rm — irreversible data deletion',
|
|
189
|
+
fix: 'Verify this is intentional. Use docker volume inspect first to check contents',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return findings;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
checkExternalVolumes,
|
|
199
|
+
checkMissingVolumeNames,
|
|
200
|
+
checkDangerousDockerCommands,
|
|
201
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Rules Tests
|
|
3
|
+
*
|
|
4
|
+
* Detects dangerous Docker patterns: external volumes,
|
|
5
|
+
* missing volume names, and destructive commands.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
checkExternalVolumes,
|
|
11
|
+
checkMissingVolumeNames,
|
|
12
|
+
checkDangerousDockerCommands,
|
|
13
|
+
} = require('./docker-rules.js');
|
|
14
|
+
|
|
15
|
+
describe('Docker Rules', () => {
|
|
16
|
+
describe('checkExternalVolumes', () => {
|
|
17
|
+
it('detects external: true in docker-compose', () => {
|
|
18
|
+
const yaml = `
|
|
19
|
+
volumes:
|
|
20
|
+
postgres_data:
|
|
21
|
+
external: true
|
|
22
|
+
`;
|
|
23
|
+
const findings = checkExternalVolumes('docker-compose.yml', yaml);
|
|
24
|
+
expect(findings).toHaveLength(1);
|
|
25
|
+
expect(findings[0].severity).toBe('block');
|
|
26
|
+
expect(findings[0].rule).toBe('no-external-volumes');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('passes without external flag', () => {
|
|
30
|
+
const yaml = `
|
|
31
|
+
volumes:
|
|
32
|
+
postgres_data:
|
|
33
|
+
name: myapp_postgres_data
|
|
34
|
+
`;
|
|
35
|
+
const findings = checkExternalVolumes('docker-compose.yml', yaml);
|
|
36
|
+
expect(findings).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('only checks docker-compose files', () => {
|
|
40
|
+
const yaml = 'external: true';
|
|
41
|
+
const findings = checkExternalVolumes('src/config.yml', yaml);
|
|
42
|
+
expect(findings).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('checkMissingVolumeNames', () => {
|
|
47
|
+
it('detects volumes without name property', () => {
|
|
48
|
+
const yaml = `
|
|
49
|
+
volumes:
|
|
50
|
+
postgres_data:
|
|
51
|
+
driver: local
|
|
52
|
+
redis_data:
|
|
53
|
+
`;
|
|
54
|
+
const findings = checkMissingVolumeNames('docker-compose.yml', yaml);
|
|
55
|
+
expect(findings).toHaveLength(2); // both volumes lack name:
|
|
56
|
+
expect(findings[0].severity).toBe('warn');
|
|
57
|
+
expect(findings[0].rule).toBe('require-volume-names');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('passes volume with explicit name', () => {
|
|
61
|
+
const yaml = `
|
|
62
|
+
volumes:
|
|
63
|
+
postgres_data:
|
|
64
|
+
name: myapp_postgres_data
|
|
65
|
+
`;
|
|
66
|
+
const findings = checkMissingVolumeNames('docker-compose.yml', yaml);
|
|
67
|
+
expect(findings).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('only checks docker-compose files', () => {
|
|
71
|
+
const yaml = 'volumes:\n data:\n driver: local';
|
|
72
|
+
const findings = checkMissingVolumeNames('src/config.ts', yaml);
|
|
73
|
+
expect(findings).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('checkDangerousDockerCommands', () => {
|
|
78
|
+
it('detects docker compose down -v', () => {
|
|
79
|
+
const script = '#!/bin/bash\ndocker compose down -v\necho "Done"';
|
|
80
|
+
const findings = checkDangerousDockerCommands('scripts/reset.sh', script);
|
|
81
|
+
expect(findings).toHaveLength(1);
|
|
82
|
+
expect(findings[0].severity).toBe('block');
|
|
83
|
+
expect(findings[0].rule).toBe('no-dangerous-docker');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('detects docker volume rm', () => {
|
|
87
|
+
const script = 'docker volume rm myapp_postgres_data';
|
|
88
|
+
const findings = checkDangerousDockerCommands('scripts/cleanup.sh', script);
|
|
89
|
+
expect(findings).toHaveLength(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('allows docker compose down without -v', () => {
|
|
93
|
+
const script = 'docker compose down\necho "Stopped"';
|
|
94
|
+
const findings = checkDangerousDockerCommands('scripts/stop.sh', script);
|
|
95
|
+
expect(findings).toHaveLength(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('checks shell scripts and CI files', () => {
|
|
99
|
+
const script = 'docker compose down -v';
|
|
100
|
+
const findings = checkDangerousDockerCommands('.github/workflows/ci.yml', script);
|
|
101
|
+
expect(findings).toHaveLength(1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|