driftdetect 0.2.2 → 0.3.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/dist/bin/drift.js +15 -1
- package/dist/bin/drift.js.map +1 -1
- package/dist/commands/boundaries.d.ts.map +1 -0
- package/dist/commands/boundaries.js +602 -0
- package/dist/commands/boundaries.js.map +1 -0
- package/dist/commands/dna/export.d.ts.map +1 -0
- package/dist/commands/dna/export.js +91 -0
- package/dist/commands/dna/export.js.map +1 -0
- package/dist/commands/dna/gene.d.ts.map +1 -0
- package/dist/commands/dna/gene.js +96 -0
- package/dist/commands/dna/gene.js.map +1 -0
- package/dist/commands/dna/index.d.ts.map +1 -0
- package/dist/commands/dna/index.js +25 -0
- package/dist/commands/dna/index.js.map +1 -0
- package/dist/commands/dna/mutations.d.ts.map +1 -0
- package/dist/commands/dna/mutations.js +111 -0
- package/dist/commands/dna/mutations.js.map +1 -0
- package/dist/commands/dna/playbook.d.ts.map +1 -0
- package/dist/commands/dna/playbook.js +67 -0
- package/dist/commands/dna/playbook.js.map +1 -0
- package/dist/commands/dna/scan.d.ts.map +1 -0
- package/dist/commands/dna/scan.js +117 -0
- package/dist/commands/dna/scan.js.map +1 -0
- package/dist/commands/dna/status.d.ts.map +1 -0
- package/dist/commands/dna/status.js +106 -0
- package/dist/commands/dna/status.js.map +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/parser.d.ts.map +1 -0
- package/dist/commands/parser.js +521 -0
- package/dist/commands/parser.js.map +1 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +118 -3
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/trends.d.ts.map +1 -0
- package/dist/commands/trends.js +115 -0
- package/dist/commands/trends.js.map +1 -0
- package/dist/commands/watch.js +2 -2
- package/dist/commands/watch.js.map +1 -1
- package/dist/services/boundary-scanner.d.ts.map +1 -0
- package/dist/services/boundary-scanner.js +374 -0
- package/dist/services/boundary-scanner.js.map +1 -0
- package/dist/services/contract-scanner.d.ts.map +1 -1
- package/dist/services/contract-scanner.js +49 -1
- package/dist/services/contract-scanner.js.map +1 -1
- package/dist/services/scanner-service.d.ts.map +1 -1
- package/dist/services/scanner-service.js +32 -1
- package/dist/services/scanner-service.js.map +1 -1
- package/package.json +6 -6
- package/dist/bin/drift.d.ts +0 -11
- package/dist/commands/approve.d.ts +0 -18
- package/dist/commands/check.d.ts +0 -39
- package/dist/commands/dashboard.d.ts +0 -16
- package/dist/commands/export.d.ts +0 -16
- package/dist/commands/files.d.ts +0 -15
- package/dist/commands/ignore.d.ts +0 -18
- package/dist/commands/index.d.ts +0 -18
- package/dist/commands/init.d.ts +0 -19
- package/dist/commands/report.d.ts +0 -20
- package/dist/commands/scan.d.ts +0 -29
- package/dist/commands/status.d.ts +0 -18
- package/dist/commands/watch.d.ts +0 -13
- package/dist/commands/where.d.ts +0 -15
- package/dist/git/hooks.d.ts +0 -108
- package/dist/git/index.d.ts +0 -6
- package/dist/git/staged-files.d.ts +0 -41
- package/dist/index.d.ts +0 -17
- package/dist/reporters/github-reporter.d.ts +0 -13
- package/dist/reporters/gitlab-reporter.d.ts +0 -16
- package/dist/reporters/index.d.ts +0 -9
- package/dist/reporters/json-reporter.d.ts +0 -13
- package/dist/reporters/text-reporter.d.ts +0 -13
- package/dist/reporters/types.d.ts +0 -42
- package/dist/services/contract-scanner.d.ts +0 -35
- package/dist/services/scanner-service.d.ts +0 -166
- package/dist/types/index.d.ts +0 -24
- package/dist/ui/index.d.ts +0 -11
- package/dist/ui/progress.d.ts +0 -115
- package/dist/ui/prompts.d.ts +0 -91
- package/dist/ui/spinner.d.ts +0 -109
- package/dist/ui/table.d.ts +0 -118
package/dist/bin/drift.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import { VERSION } from '../index.js';
|
|
12
|
-
import { initCommand, scanCommand, checkCommand, statusCommand, approveCommand, ignoreCommand, reportCommand, exportCommand, whereCommand, filesCommand, watchCommand, dashboardCommand, } from '../commands/index.js';
|
|
12
|
+
import { initCommand, scanCommand, checkCommand, statusCommand, approveCommand, ignoreCommand, reportCommand, exportCommand, whereCommand, filesCommand, watchCommand, dashboardCommand, trendsCommand, parserCommand, dnaCommand, boundariesCommand, } from '../commands/index.js';
|
|
13
13
|
/**
|
|
14
14
|
* Create and configure the main CLI program
|
|
15
15
|
*/
|
|
@@ -34,6 +34,10 @@ function createProgram() {
|
|
|
34
34
|
program.addCommand(filesCommand);
|
|
35
35
|
program.addCommand(watchCommand);
|
|
36
36
|
program.addCommand(dashboardCommand);
|
|
37
|
+
program.addCommand(trendsCommand);
|
|
38
|
+
program.addCommand(parserCommand);
|
|
39
|
+
program.addCommand(dnaCommand);
|
|
40
|
+
program.addCommand(boundariesCommand);
|
|
37
41
|
// Add help examples
|
|
38
42
|
program.addHelpText('after', `
|
|
39
43
|
Examples:
|
|
@@ -58,6 +62,16 @@ Examples:
|
|
|
58
62
|
$ drift watch --context .drift-context.md Auto-update AI context file
|
|
59
63
|
$ drift dashboard Launch the web dashboard
|
|
60
64
|
$ drift dashboard --port 8080 Launch on a custom port
|
|
65
|
+
$ drift trends View pattern regressions over time
|
|
66
|
+
$ drift trends --period 30d View trends for last 30 days
|
|
67
|
+
$ drift parser Show parser status and capabilities
|
|
68
|
+
$ drift parser --test file.py Test parsing a specific file
|
|
69
|
+
$ drift dna Show styling DNA status
|
|
70
|
+
$ drift dna scan Analyze codebase styling DNA
|
|
71
|
+
$ drift dna playbook Generate styling playbook
|
|
72
|
+
$ drift boundaries Show data access boundaries
|
|
73
|
+
$ drift boundaries tables List discovered tables
|
|
74
|
+
$ drift boundaries check Check for boundary violations
|
|
61
75
|
|
|
62
76
|
Documentation:
|
|
63
77
|
https://github.com/drift/drift
|
package/dist/bin/drift.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drift.js","sourceRoot":"","sources":["../../src/bin/drift.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EACL,WAAW,EACX,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,EACb,aAAa,EACb,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,gBAAgB,
|
|
1
|
+
{"version":3,"file":"drift.js","sourceRoot":"","sources":["../../src/bin/drift.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EACL,WAAW,EACX,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,EACb,aAAa,EACb,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,UAAU,EACV,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAE9B;;GAEG;AACH,SAAS,aAAa;IACpB,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,OAAO,CAAC;SACb,WAAW,CAAC,qEAAqE,CAAC;SAClF,OAAO,CAAC,OAAO,EAAE,eAAe,EAAE,4BAA4B,CAAC;SAC/D,MAAM,CAAC,WAAW,EAAE,uBAAuB,CAAC;SAC5C,MAAM,CAAC,YAAY,EAAE,wBAAwB,CAAC,CAAC;IAElD,wBAAwB;IACxB,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IAChC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IAChC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACjC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAClC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;IACnC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAClC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAClC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAClC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACjC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACjC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACjC,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;IACrC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAClC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAClC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IAC/B,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAEtC,oBAAoB;IACpB,OAAO,CAAC,WAAW,CACjB,OAAO,EACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCH,CACE,CAAC;IAEF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAEhC,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACzC,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBACzB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,cAAc;AACd,IAAI,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundaries.d.ts","sourceRoot":"","sources":["../../src/commands/boundaries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,MAAM,WAAW,iBAAiB;IAChC,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAgnBD;;GAEG;AACH,eAAO,MAAM,iBAAiB,SAIL,CAAC"}
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boundaries Command - drift boundaries
|
|
3
|
+
*
|
|
4
|
+
* Show data access boundaries and check for violations.
|
|
5
|
+
* Tracks which code accesses which database tables/fields.
|
|
6
|
+
*
|
|
7
|
+
* @requirements Data Boundaries Feature
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import * as fs from 'node:fs/promises';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { createBoundaryStore, } from 'driftdetect-core';
|
|
14
|
+
/** Directory name for drift configuration */
|
|
15
|
+
const DRIFT_DIR = '.drift';
|
|
16
|
+
/** Directory name for boundaries */
|
|
17
|
+
const BOUNDARIES_DIR = 'boundaries';
|
|
18
|
+
/** Rules file name */
|
|
19
|
+
const RULES_FILE = 'rules.json';
|
|
20
|
+
/**
|
|
21
|
+
* Check if boundaries directory exists
|
|
22
|
+
*/
|
|
23
|
+
async function boundariesExist(rootDir) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(path.join(rootDir, DRIFT_DIR, BOUNDARIES_DIR));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Show helpful message when boundaries not initialized
|
|
34
|
+
*/
|
|
35
|
+
function showNotInitializedMessage() {
|
|
36
|
+
console.log();
|
|
37
|
+
console.log(chalk.yellow('⚠️ No data boundaries discovered yet.'));
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(chalk.gray('Data boundaries track which code accesses which database tables.'));
|
|
40
|
+
console.log(chalk.gray('Run a scan to discover data access patterns:'));
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(chalk.cyan(' drift scan'));
|
|
43
|
+
console.log();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Overview subcommand - default view
|
|
47
|
+
*/
|
|
48
|
+
async function overviewAction(options) {
|
|
49
|
+
const rootDir = process.cwd();
|
|
50
|
+
const format = options.format ?? 'text';
|
|
51
|
+
if (!(await boundariesExist(rootDir))) {
|
|
52
|
+
if (format === 'json') {
|
|
53
|
+
console.log(JSON.stringify({ error: 'No boundaries data found' }));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
showNotInitializedMessage();
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const store = createBoundaryStore({ rootDir });
|
|
61
|
+
await store.initialize();
|
|
62
|
+
const accessMap = store.getAccessMap();
|
|
63
|
+
const sensitiveFields = store.getSensitiveAccess();
|
|
64
|
+
// JSON output
|
|
65
|
+
if (format === 'json') {
|
|
66
|
+
console.log(JSON.stringify({
|
|
67
|
+
tables: accessMap.stats.totalTables,
|
|
68
|
+
accessPoints: accessMap.stats.totalAccessPoints,
|
|
69
|
+
sensitiveFields: accessMap.stats.totalSensitiveFields,
|
|
70
|
+
models: accessMap.stats.totalModels,
|
|
71
|
+
tableList: Object.keys(accessMap.tables),
|
|
72
|
+
}, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Text output
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(chalk.bold('🗄️ Data Boundaries'));
|
|
78
|
+
console.log();
|
|
79
|
+
// Summary stats
|
|
80
|
+
console.log(`Tables Discovered: ${chalk.cyan(accessMap.stats.totalTables)}`);
|
|
81
|
+
console.log(`Access Points: ${chalk.cyan(accessMap.stats.totalAccessPoints)}`);
|
|
82
|
+
console.log(`Sensitive Fields: ${chalk.cyan(accessMap.stats.totalSensitiveFields)}`);
|
|
83
|
+
console.log();
|
|
84
|
+
// Top accessed tables
|
|
85
|
+
const tableEntries = Object.entries(accessMap.tables)
|
|
86
|
+
.map(([name, info]) => ({
|
|
87
|
+
name,
|
|
88
|
+
accessCount: info.accessedBy.length,
|
|
89
|
+
fileCount: new Set(info.accessedBy.map(ap => ap.file)).size,
|
|
90
|
+
}))
|
|
91
|
+
.sort((a, b) => b.accessCount - a.accessCount)
|
|
92
|
+
.slice(0, 5);
|
|
93
|
+
if (tableEntries.length > 0) {
|
|
94
|
+
console.log(chalk.bold('Top Accessed Tables:'));
|
|
95
|
+
for (const table of tableEntries) {
|
|
96
|
+
const name = table.name.padEnd(16);
|
|
97
|
+
console.log(` ${chalk.white(name)} ${chalk.gray(`${table.accessCount} access points (${table.fileCount} files)`)}`);
|
|
98
|
+
}
|
|
99
|
+
console.log();
|
|
100
|
+
}
|
|
101
|
+
// Sensitive field access
|
|
102
|
+
if (sensitiveFields.length > 0) {
|
|
103
|
+
console.log(chalk.bold('Sensitive Field Access:'));
|
|
104
|
+
// Group by field name and count locations
|
|
105
|
+
const fieldCounts = new Map();
|
|
106
|
+
for (const field of sensitiveFields) {
|
|
107
|
+
const key = field.table ? `${field.table}.${field.field}` : field.field;
|
|
108
|
+
fieldCounts.set(key, (fieldCounts.get(key) ?? 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
const sortedFields = Array.from(fieldCounts.entries())
|
|
111
|
+
.sort((a, b) => b[1] - a[1])
|
|
112
|
+
.slice(0, 5);
|
|
113
|
+
for (const [fieldName, count] of sortedFields) {
|
|
114
|
+
const name = fieldName.padEnd(24);
|
|
115
|
+
console.log(` ${chalk.yellow(name)} ${chalk.gray(`${count} locations`)}`);
|
|
116
|
+
}
|
|
117
|
+
console.log();
|
|
118
|
+
}
|
|
119
|
+
// Quick actions
|
|
120
|
+
console.log(chalk.gray("Run 'drift boundaries table <name>' for details"));
|
|
121
|
+
console.log();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Tables subcommand - list all discovered tables
|
|
125
|
+
*/
|
|
126
|
+
async function tablesAction(options) {
|
|
127
|
+
const rootDir = process.cwd();
|
|
128
|
+
const format = options.format ?? 'text';
|
|
129
|
+
if (!(await boundariesExist(rootDir))) {
|
|
130
|
+
if (format === 'json') {
|
|
131
|
+
console.log(JSON.stringify({ error: 'No boundaries data found', tables: [] }));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
showNotInitializedMessage();
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const store = createBoundaryStore({ rootDir });
|
|
139
|
+
await store.initialize();
|
|
140
|
+
const accessMap = store.getAccessMap();
|
|
141
|
+
// JSON output
|
|
142
|
+
if (format === 'json') {
|
|
143
|
+
const tables = Object.entries(accessMap.tables).map(([name, info]) => ({
|
|
144
|
+
name,
|
|
145
|
+
model: info.model,
|
|
146
|
+
fields: info.fields,
|
|
147
|
+
accessCount: info.accessedBy.length,
|
|
148
|
+
sensitiveFields: info.sensitiveFields.map(f => f.field),
|
|
149
|
+
}));
|
|
150
|
+
console.log(JSON.stringify({ tables }, null, 2));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Text output
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(chalk.bold('🗄️ Discovered Tables'));
|
|
156
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
157
|
+
console.log();
|
|
158
|
+
const tableEntries = Object.entries(accessMap.tables)
|
|
159
|
+
.sort((a, b) => b[1].accessedBy.length - a[1].accessedBy.length);
|
|
160
|
+
if (tableEntries.length === 0) {
|
|
161
|
+
console.log(chalk.gray(' No tables discovered yet.'));
|
|
162
|
+
console.log();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const [name, info] of tableEntries) {
|
|
166
|
+
const fileCount = new Set(info.accessedBy.map(ap => ap.file)).size;
|
|
167
|
+
const hasSensitive = info.sensitiveFields.length > 0;
|
|
168
|
+
const tableName = hasSensitive ? chalk.yellow(name) : chalk.white(name);
|
|
169
|
+
const modelInfo = info.model ? chalk.gray(` (${info.model})`) : '';
|
|
170
|
+
console.log(` ${tableName}${modelInfo}`);
|
|
171
|
+
console.log(chalk.gray(` ${info.accessedBy.length} access points in ${fileCount} files`));
|
|
172
|
+
if (info.sensitiveFields.length > 0) {
|
|
173
|
+
console.log(chalk.yellow(` ⚠ ${info.sensitiveFields.length} sensitive fields`));
|
|
174
|
+
}
|
|
175
|
+
console.log();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Table subcommand - show access to specific table
|
|
180
|
+
*/
|
|
181
|
+
async function tableAction(tableName, options) {
|
|
182
|
+
const rootDir = process.cwd();
|
|
183
|
+
const format = options.format ?? 'text';
|
|
184
|
+
if (!(await boundariesExist(rootDir))) {
|
|
185
|
+
if (format === 'json') {
|
|
186
|
+
console.log(JSON.stringify({ error: 'No boundaries data found' }));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
showNotInitializedMessage();
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const store = createBoundaryStore({ rootDir });
|
|
194
|
+
await store.initialize();
|
|
195
|
+
const tableInfo = store.getTableAccess(tableName);
|
|
196
|
+
if (!tableInfo) {
|
|
197
|
+
if (format === 'json') {
|
|
198
|
+
console.log(JSON.stringify({ error: `Table '${tableName}' not found` }));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
console.log();
|
|
202
|
+
console.log(chalk.red(`Table '${tableName}' not found.`));
|
|
203
|
+
console.log(chalk.gray("Run 'drift boundaries tables' to see all discovered tables."));
|
|
204
|
+
console.log();
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// JSON output
|
|
209
|
+
if (format === 'json') {
|
|
210
|
+
console.log(JSON.stringify({
|
|
211
|
+
name: tableInfo.name,
|
|
212
|
+
model: tableInfo.model,
|
|
213
|
+
fields: tableInfo.fields,
|
|
214
|
+
sensitiveFields: tableInfo.sensitiveFields,
|
|
215
|
+
accessPoints: tableInfo.accessedBy,
|
|
216
|
+
}, null, 2));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Text output
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(chalk.bold(`🗄️ Table: ${tableName}`));
|
|
222
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
223
|
+
console.log();
|
|
224
|
+
if (tableInfo.model) {
|
|
225
|
+
console.log(`Model: ${chalk.cyan(tableInfo.model)}`);
|
|
226
|
+
}
|
|
227
|
+
console.log(`Fields: ${chalk.gray(tableInfo.fields.join(', ') || 'none detected')}`);
|
|
228
|
+
console.log(`Access Points: ${chalk.cyan(tableInfo.accessedBy.length)}`);
|
|
229
|
+
console.log();
|
|
230
|
+
// Sensitive fields
|
|
231
|
+
if (tableInfo.sensitiveFields.length > 0) {
|
|
232
|
+
console.log(chalk.bold.yellow('Sensitive Fields:'));
|
|
233
|
+
for (const field of tableInfo.sensitiveFields) {
|
|
234
|
+
console.log(` ${chalk.yellow('⚠')} ${field.field} ${chalk.gray(`(${field.sensitivityType})`)}`);
|
|
235
|
+
}
|
|
236
|
+
console.log();
|
|
237
|
+
}
|
|
238
|
+
// Access points grouped by file
|
|
239
|
+
const byFile = new Map();
|
|
240
|
+
for (const ap of tableInfo.accessedBy) {
|
|
241
|
+
if (!byFile.has(ap.file)) {
|
|
242
|
+
byFile.set(ap.file, []);
|
|
243
|
+
}
|
|
244
|
+
byFile.get(ap.file).push(ap);
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk.bold('Access Points:'));
|
|
247
|
+
for (const [file, accessPoints] of byFile) {
|
|
248
|
+
console.log(` ${chalk.cyan(file)}`);
|
|
249
|
+
for (const ap of accessPoints) {
|
|
250
|
+
const opColor = ap.operation === 'write' ? chalk.yellow :
|
|
251
|
+
ap.operation === 'delete' ? chalk.red : chalk.gray;
|
|
252
|
+
console.log(` Line ${ap.line}: ${opColor(ap.operation)} ${chalk.gray(ap.fields.join(', ') || '')}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
console.log();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* File subcommand - show what data a file/pattern accesses
|
|
259
|
+
*/
|
|
260
|
+
async function fileAction(pattern, options) {
|
|
261
|
+
const rootDir = process.cwd();
|
|
262
|
+
const format = options.format ?? 'text';
|
|
263
|
+
if (!(await boundariesExist(rootDir))) {
|
|
264
|
+
if (format === 'json') {
|
|
265
|
+
console.log(JSON.stringify({ error: 'No boundaries data found', files: [] }));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
showNotInitializedMessage();
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const store = createBoundaryStore({ rootDir });
|
|
273
|
+
await store.initialize();
|
|
274
|
+
const fileAccess = store.getFileAccess(pattern);
|
|
275
|
+
// JSON output
|
|
276
|
+
if (format === 'json') {
|
|
277
|
+
console.log(JSON.stringify({ files: fileAccess }, null, 2));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Text output
|
|
281
|
+
console.log();
|
|
282
|
+
console.log(chalk.bold(`📁 Data Access: ${pattern}`));
|
|
283
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
284
|
+
console.log();
|
|
285
|
+
if (fileAccess.length === 0) {
|
|
286
|
+
console.log(chalk.gray(` No data access found for pattern '${pattern}'.`));
|
|
287
|
+
console.log();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
for (const fileInfo of fileAccess) {
|
|
291
|
+
console.log(chalk.cyan(fileInfo.file));
|
|
292
|
+
console.log(` Tables: ${chalk.white(fileInfo.tables.join(', '))}`);
|
|
293
|
+
console.log(` Access Points: ${fileInfo.accessPoints.length}`);
|
|
294
|
+
for (const ap of fileInfo.accessPoints) {
|
|
295
|
+
const opColor = ap.operation === 'write' ? chalk.yellow :
|
|
296
|
+
ap.operation === 'delete' ? chalk.red : chalk.gray;
|
|
297
|
+
console.log(` Line ${ap.line}: ${opColor(ap.operation)} ${chalk.white(ap.table)} ${chalk.gray(ap.fields.join(', '))}`);
|
|
298
|
+
}
|
|
299
|
+
console.log();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Sensitive subcommand - show all sensitive field access
|
|
304
|
+
*/
|
|
305
|
+
async function sensitiveAction(options) {
|
|
306
|
+
const rootDir = process.cwd();
|
|
307
|
+
const format = options.format ?? 'text';
|
|
308
|
+
if (!(await boundariesExist(rootDir))) {
|
|
309
|
+
if (format === 'json') {
|
|
310
|
+
console.log(JSON.stringify({ error: 'No boundaries data found', sensitiveFields: [] }));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
showNotInitializedMessage();
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const store = createBoundaryStore({ rootDir });
|
|
318
|
+
await store.initialize();
|
|
319
|
+
const sensitiveFields = store.getSensitiveAccess();
|
|
320
|
+
// JSON output
|
|
321
|
+
if (format === 'json') {
|
|
322
|
+
console.log(JSON.stringify({ sensitiveFields }, null, 2));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Text output
|
|
326
|
+
console.log();
|
|
327
|
+
console.log(chalk.bold('🔒 Sensitive Field Access'));
|
|
328
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
329
|
+
console.log();
|
|
330
|
+
if (sensitiveFields.length === 0) {
|
|
331
|
+
console.log(chalk.gray(' No sensitive fields detected.'));
|
|
332
|
+
console.log();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Group by sensitivity type
|
|
336
|
+
const byType = new Map();
|
|
337
|
+
for (const field of sensitiveFields) {
|
|
338
|
+
const type = field.sensitivityType;
|
|
339
|
+
if (!byType.has(type)) {
|
|
340
|
+
byType.set(type, []);
|
|
341
|
+
}
|
|
342
|
+
byType.get(type).push(field);
|
|
343
|
+
}
|
|
344
|
+
const typeColors = {
|
|
345
|
+
pii: chalk.yellow,
|
|
346
|
+
credentials: chalk.red,
|
|
347
|
+
financial: chalk.magenta,
|
|
348
|
+
health: chalk.cyan,
|
|
349
|
+
unknown: chalk.gray,
|
|
350
|
+
};
|
|
351
|
+
for (const [type, fields] of byType) {
|
|
352
|
+
const color = typeColors[type] ?? chalk.gray;
|
|
353
|
+
console.log(color.bold(`${type.toUpperCase()} (${fields.length}):`));
|
|
354
|
+
for (const field of fields) {
|
|
355
|
+
const fieldName = field.table ? `${field.table}.${field.field}` : field.field;
|
|
356
|
+
console.log(` ${color('●')} ${chalk.white(fieldName)}`);
|
|
357
|
+
console.log(chalk.gray(` ${field.file}:${field.line}`));
|
|
358
|
+
}
|
|
359
|
+
console.log();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Check subcommand - check for boundary violations
|
|
364
|
+
*/
|
|
365
|
+
async function checkAction(options) {
|
|
366
|
+
const rootDir = process.cwd();
|
|
367
|
+
const format = options.format ?? 'text';
|
|
368
|
+
if (!(await boundariesExist(rootDir))) {
|
|
369
|
+
if (format === 'json') {
|
|
370
|
+
console.log(JSON.stringify({ error: 'No boundaries data found' }));
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
showNotInitializedMessage();
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const store = createBoundaryStore({ rootDir });
|
|
378
|
+
await store.initialize();
|
|
379
|
+
const rules = store.getRules();
|
|
380
|
+
if (!rules) {
|
|
381
|
+
if (format === 'json') {
|
|
382
|
+
console.log(JSON.stringify({ error: 'No rules.json found', violations: [] }));
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
console.log();
|
|
386
|
+
console.log(chalk.yellow('⚠️ No boundary rules configured.'));
|
|
387
|
+
console.log();
|
|
388
|
+
console.log(chalk.gray('Create rules to enforce data access boundaries:'));
|
|
389
|
+
console.log(chalk.cyan(' drift boundaries init-rules'));
|
|
390
|
+
console.log();
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const violations = store.checkAllViolations();
|
|
395
|
+
// JSON output
|
|
396
|
+
if (format === 'json') {
|
|
397
|
+
console.log(JSON.stringify({
|
|
398
|
+
rulesCount: rules.boundaries.length,
|
|
399
|
+
violations,
|
|
400
|
+
violationCount: violations.length,
|
|
401
|
+
}, null, 2));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Text output
|
|
405
|
+
console.log();
|
|
406
|
+
console.log(chalk.bold('🔍 Boundary Check'));
|
|
407
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
408
|
+
console.log();
|
|
409
|
+
console.log(`Rules: ${chalk.cyan(rules.boundaries.length)}`);
|
|
410
|
+
console.log(`Violations: ${violations.length > 0 ? chalk.red(violations.length) : chalk.green(0)}`);
|
|
411
|
+
console.log();
|
|
412
|
+
if (violations.length === 0) {
|
|
413
|
+
console.log(chalk.green('✓ No boundary violations found.'));
|
|
414
|
+
console.log();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// Group violations by severity
|
|
418
|
+
const errors = violations.filter(v => v.severity === 'error');
|
|
419
|
+
const warnings = violations.filter(v => v.severity === 'warning');
|
|
420
|
+
const infos = violations.filter(v => v.severity === 'info');
|
|
421
|
+
if (errors.length > 0) {
|
|
422
|
+
console.log(chalk.red.bold(`Errors (${errors.length}):`));
|
|
423
|
+
for (const v of errors) {
|
|
424
|
+
console.log(chalk.red(` ✗ ${v.file}:${v.line}`));
|
|
425
|
+
console.log(chalk.gray(` ${v.message}`));
|
|
426
|
+
if (v.suggestion) {
|
|
427
|
+
console.log(chalk.gray(` → ${v.suggestion}`));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
console.log();
|
|
431
|
+
}
|
|
432
|
+
if (warnings.length > 0) {
|
|
433
|
+
console.log(chalk.yellow.bold(`Warnings (${warnings.length}):`));
|
|
434
|
+
for (const v of warnings) {
|
|
435
|
+
console.log(chalk.yellow(` ⚠ ${v.file}:${v.line}`));
|
|
436
|
+
console.log(chalk.gray(` ${v.message}`));
|
|
437
|
+
}
|
|
438
|
+
console.log();
|
|
439
|
+
}
|
|
440
|
+
if (infos.length > 0) {
|
|
441
|
+
console.log(chalk.blue.bold(`Info (${infos.length}):`));
|
|
442
|
+
for (const v of infos) {
|
|
443
|
+
console.log(chalk.blue(` ℹ ${v.file}:${v.line}`));
|
|
444
|
+
console.log(chalk.gray(` ${v.message}`));
|
|
445
|
+
}
|
|
446
|
+
console.log();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Init-rules subcommand - generate starter rules.json
|
|
451
|
+
*/
|
|
452
|
+
async function initRulesAction(options) {
|
|
453
|
+
const rootDir = process.cwd();
|
|
454
|
+
const format = options.format ?? 'text';
|
|
455
|
+
const rulesPath = path.join(rootDir, DRIFT_DIR, BOUNDARIES_DIR, RULES_FILE);
|
|
456
|
+
// Check if rules already exist
|
|
457
|
+
try {
|
|
458
|
+
await fs.access(rulesPath);
|
|
459
|
+
if (format === 'json') {
|
|
460
|
+
console.log(JSON.stringify({ error: 'rules.json already exists', path: rulesPath }));
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
console.log();
|
|
464
|
+
console.log(chalk.yellow(`⚠️ ${rulesPath} already exists.`));
|
|
465
|
+
console.log(chalk.gray('Delete it first if you want to regenerate.'));
|
|
466
|
+
console.log();
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// File doesn't exist, continue
|
|
472
|
+
}
|
|
473
|
+
// Ensure directory exists
|
|
474
|
+
await fs.mkdir(path.join(rootDir, DRIFT_DIR, BOUNDARIES_DIR), { recursive: true });
|
|
475
|
+
// Generate starter rules
|
|
476
|
+
const starterRules = {
|
|
477
|
+
version: '1.0',
|
|
478
|
+
sensitivity: {
|
|
479
|
+
critical: [
|
|
480
|
+
'users.password_hash',
|
|
481
|
+
'users.ssn',
|
|
482
|
+
'payments.card_number',
|
|
483
|
+
],
|
|
484
|
+
sensitive: [
|
|
485
|
+
'users.email',
|
|
486
|
+
'users.phone',
|
|
487
|
+
'users.address',
|
|
488
|
+
],
|
|
489
|
+
general: [],
|
|
490
|
+
},
|
|
491
|
+
boundaries: [
|
|
492
|
+
{
|
|
493
|
+
id: 'sensitive-data-access',
|
|
494
|
+
description: 'Sensitive user data should only be accessed from user services',
|
|
495
|
+
tables: ['users'],
|
|
496
|
+
fields: ['users.email', 'users.phone', 'users.address'],
|
|
497
|
+
allowedPaths: [
|
|
498
|
+
'**/services/user*.ts',
|
|
499
|
+
'**/services/user*.js',
|
|
500
|
+
'**/repositories/user*.ts',
|
|
501
|
+
],
|
|
502
|
+
excludePaths: ['**/*.test.ts', '**/*.spec.ts', '**/tests/**'],
|
|
503
|
+
severity: 'warning',
|
|
504
|
+
enabled: true,
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: 'credentials-access',
|
|
508
|
+
description: 'Credentials should only be accessed from auth services',
|
|
509
|
+
fields: ['users.password_hash', 'users.ssn'],
|
|
510
|
+
allowedPaths: [
|
|
511
|
+
'**/services/auth*.ts',
|
|
512
|
+
'**/auth/**',
|
|
513
|
+
],
|
|
514
|
+
excludePaths: ['**/*.test.ts', '**/*.spec.ts'],
|
|
515
|
+
severity: 'error',
|
|
516
|
+
enabled: true,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: 'payment-data-access',
|
|
520
|
+
description: 'Payment data should only be accessed from payment services',
|
|
521
|
+
tables: ['payments', 'transactions'],
|
|
522
|
+
allowedPaths: [
|
|
523
|
+
'**/services/payment*.ts',
|
|
524
|
+
'**/services/billing*.ts',
|
|
525
|
+
'**/payments/**',
|
|
526
|
+
],
|
|
527
|
+
excludePaths: ['**/*.test.ts', '**/*.spec.ts'],
|
|
528
|
+
severity: 'error',
|
|
529
|
+
enabled: true,
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
globalExcludes: [
|
|
533
|
+
'**/node_modules/**',
|
|
534
|
+
'**/dist/**',
|
|
535
|
+
'**/build/**',
|
|
536
|
+
'**/*.d.ts',
|
|
537
|
+
],
|
|
538
|
+
};
|
|
539
|
+
await fs.writeFile(rulesPath, JSON.stringify(starterRules, null, 2));
|
|
540
|
+
// JSON output
|
|
541
|
+
if (format === 'json') {
|
|
542
|
+
console.log(JSON.stringify({ success: true, path: rulesPath, rules: starterRules }, null, 2));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Text output
|
|
546
|
+
console.log();
|
|
547
|
+
console.log(chalk.green('✓ Created starter rules.json'));
|
|
548
|
+
console.log();
|
|
549
|
+
console.log(chalk.gray(`Location: ${rulesPath}`));
|
|
550
|
+
console.log();
|
|
551
|
+
console.log(chalk.bold('Included rules:'));
|
|
552
|
+
for (const rule of starterRules.boundaries) {
|
|
553
|
+
const severityColor = rule.severity === 'error' ? chalk.red :
|
|
554
|
+
rule.severity === 'warning' ? chalk.yellow : chalk.blue;
|
|
555
|
+
console.log(` ${severityColor('●')} ${rule.id}`);
|
|
556
|
+
console.log(chalk.gray(` ${rule.description}`));
|
|
557
|
+
}
|
|
558
|
+
console.log();
|
|
559
|
+
console.log(chalk.gray('Edit the rules.json file to customize boundaries for your project.'));
|
|
560
|
+
console.log(chalk.gray("Then run 'drift boundaries check' to validate."));
|
|
561
|
+
console.log();
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Create the boundaries command with subcommands
|
|
565
|
+
*/
|
|
566
|
+
export const boundariesCommand = new Command('boundaries')
|
|
567
|
+
.description('Show data access boundaries and check for violations')
|
|
568
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
569
|
+
.option('--verbose', 'Enable verbose output')
|
|
570
|
+
.action(overviewAction);
|
|
571
|
+
// Subcommands
|
|
572
|
+
boundariesCommand
|
|
573
|
+
.command('tables')
|
|
574
|
+
.description('List all discovered tables')
|
|
575
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
576
|
+
.action(tablesAction);
|
|
577
|
+
boundariesCommand
|
|
578
|
+
.command('table <name>')
|
|
579
|
+
.description('Show access to a specific table')
|
|
580
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
581
|
+
.action(tableAction);
|
|
582
|
+
boundariesCommand
|
|
583
|
+
.command('file <pattern>')
|
|
584
|
+
.description('Show what data a file or pattern accesses')
|
|
585
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
586
|
+
.action(fileAction);
|
|
587
|
+
boundariesCommand
|
|
588
|
+
.command('sensitive')
|
|
589
|
+
.description('Show all sensitive field access')
|
|
590
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
591
|
+
.action(sensitiveAction);
|
|
592
|
+
boundariesCommand
|
|
593
|
+
.command('check')
|
|
594
|
+
.description('Check for boundary violations (requires rules.json)')
|
|
595
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
596
|
+
.action(checkAction);
|
|
597
|
+
boundariesCommand
|
|
598
|
+
.command('init-rules')
|
|
599
|
+
.description('Generate a starter rules.json file')
|
|
600
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
601
|
+
.action(initRulesAction);
|
|
602
|
+
//# sourceMappingURL=boundaries.js.map
|