korext 0.9.5 → 0.9.7
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/bin/korext.js +788 -22
- package/package.json +1 -1
package/bin/korext.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Command } from 'commander';
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import fetch from 'node-fetch';
|
|
9
|
+
import { createInterface } from 'readline';
|
|
9
10
|
|
|
10
11
|
// Load version
|
|
11
12
|
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
@@ -29,9 +30,9 @@ function getConfig() {
|
|
|
29
30
|
|
|
30
31
|
function saveConfig(config) {
|
|
31
32
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
32
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
33
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
33
34
|
}
|
|
34
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
35
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
let _deprecationWarned = false;
|
|
@@ -57,6 +58,203 @@ function getLanguageFromExt(ext) {
|
|
|
57
58
|
return map[ext] || 'plaintext';
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// ─── TAXONOMY BUILDER ─────────────────────────────────────────────────────────
|
|
62
|
+
// Standalone copy of derivation logic from src/lib/packTaxonomy.ts.
|
|
63
|
+
// Operates on cached rule definitions (packs with tags).
|
|
64
|
+
|
|
65
|
+
const INDUSTRY_LABELS = {
|
|
66
|
+
finance: 'Finance',
|
|
67
|
+
healthcare: 'Healthcare',
|
|
68
|
+
defense: 'Defense',
|
|
69
|
+
government: 'Government',
|
|
70
|
+
aerospace: 'Aerospace',
|
|
71
|
+
energy: 'Energy',
|
|
72
|
+
ecommerce: 'E-Commerce',
|
|
73
|
+
technology: 'Technology',
|
|
74
|
+
education: 'Education and Research',
|
|
75
|
+
insurance: 'Insurance',
|
|
76
|
+
automotive: 'Automotive',
|
|
77
|
+
telecom: 'Telecom',
|
|
78
|
+
transport: 'Transport and Maritime',
|
|
79
|
+
manufacturing: 'Manufacturing',
|
|
80
|
+
lifesciences: 'Life Sciences',
|
|
81
|
+
critical_infrastructure: 'Critical Infrastructure',
|
|
82
|
+
legal: 'Legal',
|
|
83
|
+
general: 'General',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const INDUSTRY_DESCRIPTIONS = {
|
|
87
|
+
finance: 'PCI-DSS, DORA, Basel III, SWIFT',
|
|
88
|
+
healthcare: 'HIPAA, FDA, FHIR, GxP',
|
|
89
|
+
defense: 'CMMC, NIST 800-171, ITAR',
|
|
90
|
+
government: 'FedRAMP, FISMA, CISA',
|
|
91
|
+
aerospace: 'DO-178C, NASA, EASA, ICAO',
|
|
92
|
+
energy: 'NERC CIP, IEC 62443, NRC',
|
|
93
|
+
critical_infrastructure: 'IEC 62443, NERC, NIST CSF',
|
|
94
|
+
manufacturing: 'IEC 62443, EN Standards',
|
|
95
|
+
ecommerce: 'PCI-DSS, GDPR Public',
|
|
96
|
+
lifesciences: 'GxP, ICH, EMA',
|
|
97
|
+
education: 'FERPA, UKRI',
|
|
98
|
+
insurance: 'NAIC, Solvency II',
|
|
99
|
+
automotive: 'ISO 26262, WP.29',
|
|
100
|
+
telecom: '3GPP, Ofcom',
|
|
101
|
+
transport: 'IMO Maritime, ITS Transport',
|
|
102
|
+
legal: 'SRA Compliance',
|
|
103
|
+
technology: 'Age Encryption, ASP.NET',
|
|
104
|
+
general: 'OWASP, Web Security, AI Safety',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const REGION_LABELS = {
|
|
108
|
+
global: 'Global',
|
|
109
|
+
us: 'United States',
|
|
110
|
+
eu: 'European Union',
|
|
111
|
+
uk: 'United Kingdom',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const INDUSTRY_MERGES = {
|
|
115
|
+
maritime: 'transport',
|
|
116
|
+
research: 'education',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const RECOMMENDED_PACKS = {
|
|
120
|
+
finance: ['pci-dss-v1', 'dora-financial-v1', 'swift-cscf-v1'],
|
|
121
|
+
healthcare: ['hipaa-hitech-v1', 'fda-21cfr-v1', 'hl7-fhir-v1'],
|
|
122
|
+
defense: ['cmmc-level2-v1', 'nist-sp800-171-v1', 'itar-ear-v1'],
|
|
123
|
+
government: ['fedramp-moderate-v1', 'fisma-v1', 'nist-csf2-v1'],
|
|
124
|
+
aerospace: ['do-178c-v1', 'nasa-7150-v1', 'easa-part21-v1'],
|
|
125
|
+
energy: ['nerc-cip-v1', 'iec-62443-v1', 'ofgem-v1'],
|
|
126
|
+
critical_infrastructure: ['iec-62443-v1', 'nerc-cip-v1', 'nist-csf2-v1'],
|
|
127
|
+
manufacturing: ['iec-62443-v1', 'en-standards-v1'],
|
|
128
|
+
ecommerce: ['pci-dss-v1', 'gdpr-public-v1'],
|
|
129
|
+
lifesciences: ['gxp-pharma-v1', 'ich-pharma-v1'],
|
|
130
|
+
education: ['ferpa-education-v1', 'ukri-research-v1'],
|
|
131
|
+
insurance: ['naic-insurance-v1', 'solvency-v1'],
|
|
132
|
+
automotive: ['iso-26262-v1', 'wp29-automotive-v1'],
|
|
133
|
+
telecom: ['3gpp-telecom-v1', 'ofcom-telecom-v1'],
|
|
134
|
+
transport: ['its-transport-v1', 'imo-maritime-v1'],
|
|
135
|
+
legal: ['sra-legal-v1'],
|
|
136
|
+
technology: ['age-encryption-v1', 'asp-security-v1'],
|
|
137
|
+
general: ['web', 'security'],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const CROSS_CUTTING_DEFAULT_ON = ['web'];
|
|
141
|
+
const CROSS_CUTTING_AVAILABLE = [
|
|
142
|
+
'browser-gov-v1', 'euai-act-v2', 'gdpr-privacy-v1', 'gpc-privacy-v1',
|
|
143
|
+
'iso-27001-v1', 'nist-csf2-v1', 'quantum-safe-v1',
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
function resolveIndustryTag(tag) {
|
|
147
|
+
return INDUSTRY_MERGES[tag] || tag;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildTaxonomyFromPacks(packs) {
|
|
151
|
+
const industries = {};
|
|
152
|
+
const regions = {};
|
|
153
|
+
|
|
154
|
+
for (const key of Object.keys(INDUSTRY_LABELS)) {
|
|
155
|
+
industries[key] = { label: INDUSTRY_LABELS[key], description: INDUSTRY_DESCRIPTIONS[key] || '', packIds: [], recommended: [] };
|
|
156
|
+
}
|
|
157
|
+
for (const key of Object.keys(REGION_LABELS)) {
|
|
158
|
+
regions[key] = { label: REGION_LABELS[key], packIds: [] };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const crossCuttingSet = new Set([...CROSS_CUTTING_DEFAULT_ON, ...CROSS_CUTTING_AVAILABLE]);
|
|
162
|
+
|
|
163
|
+
for (const [packId, pack] of Object.entries(packs)) {
|
|
164
|
+
const rawIndustries = pack.tags?.industries || [];
|
|
165
|
+
const rawRegions = pack.tags?.regions || [];
|
|
166
|
+
|
|
167
|
+
if (crossCuttingSet.has(packId)) {
|
|
168
|
+
const pr = rawRegions.length > 0 ? rawRegions : ['global'];
|
|
169
|
+
for (const r of pr) {
|
|
170
|
+
if (!regions[r]) regions[r] = { label: REGION_LABELS[r] || r, packIds: [] };
|
|
171
|
+
if (!regions[r].packIds.includes(packId)) regions[r].packIds.push(packId);
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let resolved;
|
|
177
|
+
if (rawIndustries.length === 0) {
|
|
178
|
+
resolved = ['general'];
|
|
179
|
+
} else {
|
|
180
|
+
const filtered = rawIndustries.filter(t => t !== 'all').map(resolveIndustryTag);
|
|
181
|
+
resolved = [...new Set(filtered)];
|
|
182
|
+
if (resolved.length === 0) resolved = ['general'];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const ind of resolved) {
|
|
186
|
+
if (!industries[ind]) {
|
|
187
|
+
industries[ind] = { label: INDUSTRY_LABELS[ind] || ind, description: INDUSTRY_DESCRIPTIONS[ind] || '', packIds: [], recommended: [] };
|
|
188
|
+
}
|
|
189
|
+
if (!industries[ind].packIds.includes(packId)) industries[ind].packIds.push(packId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const pr = rawRegions.length > 0 ? rawRegions : ['global'];
|
|
193
|
+
for (const r of pr) {
|
|
194
|
+
if (!regions[r]) regions[r] = { label: REGION_LABELS[r] || r, packIds: [] };
|
|
195
|
+
if (!regions[r].packIds.includes(packId)) regions[r].packIds.push(packId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Apply recommended
|
|
200
|
+
for (const [ind, recList] of Object.entries(RECOMMENDED_PACKS)) {
|
|
201
|
+
if (industries[ind]) {
|
|
202
|
+
industries[ind].recommended = recList.filter(pid => industries[ind].packIds.includes(pid));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Remove empties
|
|
207
|
+
for (const key of Object.keys(industries)) {
|
|
208
|
+
if (industries[key].packIds.length === 0) delete industries[key];
|
|
209
|
+
}
|
|
210
|
+
for (const key of Object.keys(regions)) {
|
|
211
|
+
if (regions[key].packIds.length === 0) delete regions[key];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Sort
|
|
215
|
+
for (const g of Object.values(industries)) g.packIds.sort();
|
|
216
|
+
for (const g of Object.values(regions)) g.packIds.sort();
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
industries,
|
|
220
|
+
regions,
|
|
221
|
+
crossCutting: {
|
|
222
|
+
defaultOn: CROSS_CUTTING_DEFAULT_ON.filter(pid => pid in packs),
|
|
223
|
+
available: CROSS_CUTTING_AVAILABLE.filter(pid => pid in packs),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resolvePacksForIndustryRegion(industryFilter, regionFilter, taxonomy) {
|
|
229
|
+
const matchedPacks = new Set();
|
|
230
|
+
const industryIds = industryFilter.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
231
|
+
const regionIds = regionFilter ? regionFilter.split(',').map(s => s.trim().toLowerCase()).filter(Boolean) : [];
|
|
232
|
+
|
|
233
|
+
for (const indId of industryIds) {
|
|
234
|
+
const group = taxonomy.industries[indId];
|
|
235
|
+
if (!group) continue;
|
|
236
|
+
for (const pid of group.packIds) {
|
|
237
|
+
if (regionIds.length === 0) {
|
|
238
|
+
matchedPacks.add(pid);
|
|
239
|
+
} else {
|
|
240
|
+
// Only include packs whose region overlaps with the filter
|
|
241
|
+
const packRegions = taxonomy.regions;
|
|
242
|
+
let inRegion = false;
|
|
243
|
+
for (const r of regionIds) {
|
|
244
|
+
if (packRegions[r] && packRegions[r].packIds.includes(pid)) inRegion = true;
|
|
245
|
+
if (packRegions['global'] && packRegions['global'].packIds.includes(pid)) inRegion = true;
|
|
246
|
+
}
|
|
247
|
+
if (inRegion) matchedPacks.add(pid);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Include cross-cutting defaults
|
|
253
|
+
for (const pid of taxonomy.crossCutting.defaultOn) matchedPacks.add(pid);
|
|
254
|
+
|
|
255
|
+
return [...matchedPacks];
|
|
256
|
+
}
|
|
257
|
+
|
|
60
258
|
// ─── LOCAL ENGINE: Rule Definition Caching + Offline Analysis ─────────────────
|
|
61
259
|
|
|
62
260
|
const RULES_CACHE_FILE = path.join(CONFIG_DIR, 'rule-definitions.json');
|
|
@@ -223,7 +421,9 @@ program
|
|
|
223
421
|
program
|
|
224
422
|
.command('packs list')
|
|
225
423
|
.description('List available policy packs from the server')
|
|
226
|
-
.
|
|
424
|
+
.option('--industry <name>', 'Filter packs by industry (e.g. finance)')
|
|
425
|
+
.option('--region <name>', 'Filter packs by region (e.g. us, eu)')
|
|
426
|
+
.action(async (_sub, options) => {
|
|
227
427
|
const token = getToken();
|
|
228
428
|
const spinner = ora('Fetching policy packs...').start();
|
|
229
429
|
try {
|
|
@@ -233,15 +433,62 @@ program
|
|
|
233
433
|
if (res.ok) {
|
|
234
434
|
const data = await res.json();
|
|
235
435
|
spinner.stop();
|
|
236
|
-
|
|
436
|
+
|
|
437
|
+
let packs = data.packs || [];
|
|
438
|
+
|
|
439
|
+
// Apply industry/region filter from taxonomy if cached definitions exist
|
|
440
|
+
if (options.industry || options.region) {
|
|
441
|
+
const defs = getRuleDefinitionsCache();
|
|
442
|
+
if (defs && defs.packs) {
|
|
443
|
+
const taxonomy = buildTaxonomyFromPacks(defs.packs);
|
|
444
|
+
const matchedIds = resolvePacksForIndustryRegion(
|
|
445
|
+
options.industry || Object.keys(taxonomy.industries).join(','),
|
|
446
|
+
options.region || null,
|
|
447
|
+
taxonomy
|
|
448
|
+
);
|
|
449
|
+
const matchedSet = new Set(matchedIds);
|
|
450
|
+
packs = packs.filter(p => matchedSet.has(p.packId));
|
|
451
|
+
} else {
|
|
452
|
+
console.log(chalk.yellow('No cached rule definitions found. Run korext rules sync for industry/region filtering.'));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const filterStr = [options.industry, options.region].filter(Boolean).join(', ');
|
|
457
|
+
console.log(chalk.bold.hex('#F27D26')(`\n\u25b2 AVAILABLE KOREXT POLICY PACKS${filterStr ? ` (filtered: ${filterStr})` : ''}`));
|
|
237
458
|
console.log(chalk.dim('======================================='));
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
459
|
+
|
|
460
|
+
if (packs.length > 0) {
|
|
461
|
+
// Group by industry if taxonomy is available
|
|
462
|
+
const defs = getRuleDefinitionsCache();
|
|
463
|
+
if (defs && defs.packs) {
|
|
464
|
+
const taxonomy = buildTaxonomyFromPacks(defs.packs);
|
|
465
|
+
const recSets = {};
|
|
466
|
+
for (const [indId, group] of Object.entries(taxonomy.industries)) {
|
|
467
|
+
recSets[indId] = new Set(group.recommended);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
for (const p of packs) {
|
|
471
|
+
// Find industries this pack belongs to
|
|
472
|
+
const packIndustries = [];
|
|
473
|
+
for (const [indId, group] of Object.entries(taxonomy.industries)) {
|
|
474
|
+
if (group.packIds.includes(p.packId)) packIndustries.push(indId);
|
|
475
|
+
}
|
|
476
|
+
const indLabel = packIndustries.map(i => INDUSTRY_LABELS[i] || i).join(', ') || 'Cross-cutting';
|
|
477
|
+
const isRec = packIndustries.some(i => recSets[i]?.has(p.packId));
|
|
478
|
+
const recTag = isRec ? chalk.green(' [recommended]') : '';
|
|
479
|
+
console.log(`${chalk.bold(p.packId)} ${chalk.dim('|')} ${p.packName || p.packId} ${chalk.dim('|')} ${chalk.cyan(indLabel)}${recTag}${p.isEnterprise ? chalk.bgRed.white(' ENTERPRISE ') : ''}`);
|
|
480
|
+
console.log(chalk.dim(` ${p.description || 'No description provided'}\n`));
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
packs.forEach((p) => {
|
|
484
|
+
console.log(`${chalk.bold(p.packId)} - ${p.packName || p.packId} ${p.isEnterprise ? chalk.bgRed.white(' ENTERPRISE ') : ''}`);
|
|
485
|
+
console.log(chalk.dim(` ${p.description || 'No description provided'}\n`));
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
console.log(chalk.dim(`Total: ${packs.length} packs`));
|
|
243
490
|
} else {
|
|
244
|
-
console.log(chalk.dim('No policy packs
|
|
491
|
+
console.log(chalk.dim('No policy packs match the filters.'));
|
|
245
492
|
}
|
|
246
493
|
} else {
|
|
247
494
|
spinner.fail(chalk.red('Failed to fetch packs. Check authentication.'));
|
|
@@ -251,19 +498,411 @@ program
|
|
|
251
498
|
}
|
|
252
499
|
});
|
|
253
500
|
|
|
501
|
+
// ─── KOREXT INDUSTRIES COMMAND ─────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
program
|
|
504
|
+
.command('industries')
|
|
505
|
+
.description('List all supported industries and their associated policy packs')
|
|
506
|
+
.action(async () => {
|
|
507
|
+
const defs = getRuleDefinitionsCache();
|
|
508
|
+
if (!defs || !defs.packs) {
|
|
509
|
+
const spinner = ora('Fetching rule definitions...').start();
|
|
510
|
+
try {
|
|
511
|
+
await fetchAndCacheRules();
|
|
512
|
+
spinner.succeed('Rule definitions cached.');
|
|
513
|
+
} catch (e) {
|
|
514
|
+
spinner.fail(chalk.red('Failed to fetch rule definitions. Run korext rules sync while online.'));
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const cachedDefs = getRuleDefinitionsCache();
|
|
520
|
+
const taxonomy = buildTaxonomyFromPacks(cachedDefs.packs);
|
|
521
|
+
|
|
522
|
+
console.log(chalk.bold.hex('#F27D26')('\n\u25b2 KOREXT INDUSTRY TAXONOMY'));
|
|
523
|
+
console.log(chalk.dim('=======================================\n'));
|
|
524
|
+
|
|
525
|
+
const sortedIndustries = Object.entries(taxonomy.industries)
|
|
526
|
+
.sort(([, a], [, b]) => a.label.localeCompare(b.label));
|
|
527
|
+
|
|
528
|
+
for (const [id, group] of sortedIndustries) {
|
|
529
|
+
const desc = INDUSTRY_DESCRIPTIONS[id] || '';
|
|
530
|
+
const recCount = group.recommended.length;
|
|
531
|
+
console.log(` ${chalk.bold.cyan(group.label)} ${chalk.dim(`(${id})`)}`);
|
|
532
|
+
console.log(` ${chalk.dim(desc)}`);
|
|
533
|
+
console.log(` ${chalk.white(`${group.packIds.length} packs`)}${recCount > 0 ? chalk.green(` (${recCount} recommended)`) : ''}`);
|
|
534
|
+
console.log(` ${chalk.dim(group.packIds.join(', '))}\n`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Show regions
|
|
538
|
+
console.log(chalk.bold('\n Regions:'));
|
|
539
|
+
for (const [id, group] of Object.entries(taxonomy.regions)) {
|
|
540
|
+
console.log(` ${chalk.cyan(group.label)} ${chalk.dim(`(${id})`)} ${chalk.dim(`${group.packIds.length} packs`)}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Show cross-cutting
|
|
544
|
+
console.log(chalk.bold('\n Cross-cutting packs:'));
|
|
545
|
+
console.log(` Default: ${chalk.green(taxonomy.crossCutting.defaultOn.join(', ') || 'none')}`);
|
|
546
|
+
console.log(` Available: ${chalk.dim(taxonomy.crossCutting.available.join(', ') || 'none')}`);
|
|
547
|
+
|
|
548
|
+
console.log(`\n ${chalk.dim('Use:')} ${chalk.green('korext enforce . --industry finance')} ${chalk.dim('to enforce all finance packs.')}`);
|
|
549
|
+
console.log(` ${chalk.dim('Use:')} ${chalk.green('korext enforce . --industry finance --region eu')} ${chalk.dim('to filter by region.\n')}`);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// ─── KOREXT INIT COMMAND ───────────────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
program
|
|
555
|
+
.command('init')
|
|
556
|
+
.description('Initialize a korext.json configuration file for your project')
|
|
557
|
+
.option('--non-interactive', 'Skip prompts and use defaults', false)
|
|
558
|
+
.action(async (options) => {
|
|
559
|
+
console.log(chalk.bold.hex('#F27D26')('\n\u25b2 KOREXT PROJECT INIT'));
|
|
560
|
+
console.log(chalk.dim('=======================================\n'));
|
|
561
|
+
|
|
562
|
+
const outputPath = path.resolve('korext.json');
|
|
563
|
+
|
|
564
|
+
if (fs.existsSync(outputPath)) {
|
|
565
|
+
console.log(chalk.yellow(`korext.json already exists at ${outputPath}`));
|
|
566
|
+
console.log(chalk.dim('Delete it manually to re-initialize.\n'));
|
|
567
|
+
process.exit(0);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Ensure rule definitions are cached
|
|
571
|
+
let defs = getRuleDefinitionsCache();
|
|
572
|
+
if (!defs || !defs.packs) {
|
|
573
|
+
const spinner = ora('Fetching rule definitions...').start();
|
|
574
|
+
try {
|
|
575
|
+
defs = await fetchAndCacheRules();
|
|
576
|
+
spinner.succeed('Rule definitions cached.');
|
|
577
|
+
} catch (e) {
|
|
578
|
+
spinner.fail(chalk.red('Failed to fetch rule definitions. Unable to resolve industries.'));
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const taxonomy = buildTaxonomyFromPacks(defs.packs);
|
|
584
|
+
|
|
585
|
+
if (options.nonInteractive) {
|
|
586
|
+
// Non-interactive: default to web pack
|
|
587
|
+
const config = {
|
|
588
|
+
targetPacks: ['web'],
|
|
589
|
+
exclude: ['node_modules', 'dist', 'build', '.next']
|
|
590
|
+
};
|
|
591
|
+
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2));
|
|
592
|
+
console.log(chalk.green(`Created ${outputPath} with default pack: web\n`));
|
|
593
|
+
process.exit(0);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Interactive prompts
|
|
597
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
598
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
// Show industries
|
|
602
|
+
console.log(chalk.bold(' Available industries:\n'));
|
|
603
|
+
const sortedIndustries = Object.entries(taxonomy.industries)
|
|
604
|
+
.filter(([id]) => id !== 'general')
|
|
605
|
+
.sort(([, a], [, b]) => a.label.localeCompare(b.label));
|
|
606
|
+
|
|
607
|
+
sortedIndustries.forEach(([id, group], i) => {
|
|
608
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.cyan(group.label)} ${chalk.dim(`(${id})`)} ${chalk.dim(INDUSTRY_DESCRIPTIONS[id] || '')}`);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
console.log();
|
|
612
|
+
const industryInput = await ask(chalk.bold(' Select industries (comma-separated numbers or IDs): '));
|
|
613
|
+
|
|
614
|
+
// Parse user input (numbers or IDs)
|
|
615
|
+
const selectedIndustries = industryInput.split(',').map(s => s.trim()).filter(Boolean).map(s => {
|
|
616
|
+
const num = parseInt(s, 10);
|
|
617
|
+
if (!isNaN(num) && num >= 1 && num <= sortedIndustries.length) {
|
|
618
|
+
return sortedIndustries[num - 1][0];
|
|
619
|
+
}
|
|
620
|
+
return s.toLowerCase();
|
|
621
|
+
}).filter(id => taxonomy.industries[id]);
|
|
622
|
+
|
|
623
|
+
if (selectedIndustries.length === 0) {
|
|
624
|
+
console.log(chalk.yellow('\n No valid industries selected. Using default: web\n'));
|
|
625
|
+
const config = {
|
|
626
|
+
targetPacks: ['web'],
|
|
627
|
+
exclude: ['node_modules', 'dist', 'build', '.next']
|
|
628
|
+
};
|
|
629
|
+
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2));
|
|
630
|
+
console.log(chalk.green(` Created ${outputPath}\n`));
|
|
631
|
+
rl.close();
|
|
632
|
+
process.exit(0);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Show regions
|
|
636
|
+
console.log(chalk.bold('\n Available regions:\n'));
|
|
637
|
+
const regionEntries = Object.entries(taxonomy.regions).sort(([a], [b]) => a.localeCompare(b));
|
|
638
|
+
regionEntries.forEach(([id, group], i) => {
|
|
639
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.cyan(group.label)} ${chalk.dim(`(${id})`)}`);
|
|
640
|
+
});
|
|
641
|
+
console.log(` ${chalk.dim('0.')} All regions`);
|
|
642
|
+
console.log();
|
|
643
|
+
const regionInput = await ask(chalk.bold(' Select region (number or ID, default: all): '));
|
|
644
|
+
|
|
645
|
+
let selectedRegion = null;
|
|
646
|
+
if (regionInput.trim()) {
|
|
647
|
+
const num = parseInt(regionInput, 10);
|
|
648
|
+
if (num === 0) {
|
|
649
|
+
selectedRegion = null;
|
|
650
|
+
} else if (!isNaN(num) && num >= 1 && num <= regionEntries.length) {
|
|
651
|
+
selectedRegion = regionEntries[num - 1][0];
|
|
652
|
+
} else {
|
|
653
|
+
const lower = regionInput.trim().toLowerCase();
|
|
654
|
+
if (taxonomy.regions[lower]) selectedRegion = lower;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Resolve packs
|
|
659
|
+
const resolvedPacks = resolvePacksForIndustryRegion(
|
|
660
|
+
selectedIndustries.join(','),
|
|
661
|
+
selectedRegion,
|
|
662
|
+
taxonomy
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
console.log(chalk.green(`\n Resolved ${resolvedPacks.length} packs for ${selectedIndustries.map(id => INDUSTRY_LABELS[id]).join(', ')}${selectedRegion ? ` (${REGION_LABELS[selectedRegion] || selectedRegion})` : ''}:`));
|
|
666
|
+
for (const pid of resolvedPacks) {
|
|
667
|
+
console.log(` ${chalk.cyan(pid)}`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const config = {
|
|
671
|
+
industry: selectedIndustries.join(','),
|
|
672
|
+
...(selectedRegion && { region: selectedRegion }),
|
|
673
|
+
targetPacks: resolvedPacks,
|
|
674
|
+
exclude: ['node_modules', 'dist', 'build', '.next']
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2));
|
|
678
|
+
console.log(chalk.green(`\n Created ${outputPath}\n`));
|
|
679
|
+
console.log(chalk.dim(` Run: ${chalk.green('korext enforce .')} to enforce these packs.\n`));
|
|
680
|
+
|
|
681
|
+
rl.close();
|
|
682
|
+
} catch (e) {
|
|
683
|
+
rl.close();
|
|
684
|
+
console.error(chalk.red(`Init failed: ${e.message}`));
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// ─── KOREXT POLICY COMMAND ────────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
program
|
|
692
|
+
.command('org-policy')
|
|
693
|
+
.description('Display the organisation governance policy for your account')
|
|
694
|
+
.action(async () => {
|
|
695
|
+
console.log(chalk.bold.hex('#F27D26')('\n\u25b2 KOREXT ORGANISATION POLICY'));
|
|
696
|
+
console.log(chalk.dim('=======================================\n'));
|
|
697
|
+
|
|
698
|
+
const token = getToken();
|
|
699
|
+
if (!token) {
|
|
700
|
+
console.log(chalk.yellow('Not authenticated. Run korext auth to sign in.'));
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const spinner = ora('Fetching org policy...').start();
|
|
705
|
+
try {
|
|
706
|
+
const res = await fetch(`${API_URL}/api/org/policy`, {
|
|
707
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
708
|
+
});
|
|
709
|
+
if (!res.ok) {
|
|
710
|
+
spinner.fail(chalk.red(`Server returned ${res.status}: ${res.statusText}`));
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
const data = await res.json();
|
|
714
|
+
spinner.succeed('Organisation policy loaded.\n');
|
|
715
|
+
|
|
716
|
+
const hasMandated = data.mandatedPacks && data.mandatedPacks.length > 0;
|
|
717
|
+
const hasRecommended = data.recommendedPacks && data.recommendedPacks.length > 0;
|
|
718
|
+
const hasAllowed = data.allowedPacks && data.allowedPacks.length > 0;
|
|
719
|
+
|
|
720
|
+
if (!hasMandated && !hasRecommended && !hasAllowed && !data.industry) {
|
|
721
|
+
console.log(chalk.dim(' No organisation policy configured.'));
|
|
722
|
+
console.log(chalk.dim(' Set one at https://app.korext.com under Org Policy (ADMIN only).\n'));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (data.industry) console.log(` ${chalk.bold('Industry:')} ${data.industry}`);
|
|
727
|
+
if (data.region) console.log(` ${chalk.bold('Region:')} ${data.region}`);
|
|
728
|
+
if (data.updatedAt) console.log(` ${chalk.bold('Updated:')} ${data.updatedAt}`);
|
|
729
|
+
console.log('');
|
|
730
|
+
|
|
731
|
+
if (hasMandated) {
|
|
732
|
+
console.log(chalk.red.bold(' MANDATED PACKS (locked):'));
|
|
733
|
+
for (const pid of data.mandatedPacks) {
|
|
734
|
+
console.log(` ${chalk.red('\u25cf')} ${pid}`);
|
|
735
|
+
}
|
|
736
|
+
console.log('');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (hasRecommended) {
|
|
740
|
+
console.log(chalk.green.bold(' RECOMMENDED PACKS:'));
|
|
741
|
+
for (const pid of data.recommendedPacks) {
|
|
742
|
+
console.log(` ${chalk.green('\u25cb')} ${pid}`);
|
|
743
|
+
}
|
|
744
|
+
console.log('');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (hasAllowed) {
|
|
748
|
+
console.log(chalk.yellow.bold(' ALLOWED PACKS:'));
|
|
749
|
+
for (const pid of data.allowedPacks) {
|
|
750
|
+
console.log(` ${chalk.yellow('\u25cb')} ${pid}`);
|
|
751
|
+
}
|
|
752
|
+
console.log('');
|
|
753
|
+
} else {
|
|
754
|
+
console.log(chalk.dim(' Allowed: all packs (no restrictions)\n'));
|
|
755
|
+
}
|
|
756
|
+
} catch (e) {
|
|
757
|
+
spinner.fail(chalk.red(`Failed: ${e.message}`));
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
|
|
254
763
|
program
|
|
255
764
|
.command('enforce [dir]')
|
|
256
765
|
.description('Statically analyze files in a directory against Korext policies')
|
|
257
|
-
.option('-p, --pack <
|
|
766
|
+
.option('-p, --pack <packIds>', 'Policy Pack ID(s) to enforce (comma-separated, e.g. web,pci-dss-v1)')
|
|
258
767
|
.option('-f, --format <format>', 'Output format (text, json, sarif)', 'text')
|
|
768
|
+
.option('--industry <industries>', 'Industry filter (comma separated, e.g. finance,healthcare)')
|
|
769
|
+
.option('--region <regions>', 'Region filter (comma separated, e.g. us,eu)')
|
|
259
770
|
.option('--offline', 'Force local-only analysis using cached rule definitions (no server calls)', false)
|
|
260
771
|
.option('--sync-rules', 'Fetch and cache latest rule definitions before running analysis', false)
|
|
772
|
+
.option('--sign', 'Request HMAC signed proof bundles (requires authentication)', false)
|
|
773
|
+
.option('--sovereignty-region <region>', 'Data sovereignty region: us, eu, or apac (routes data to regional database)')
|
|
261
774
|
.action(async (dirArg, options) => {
|
|
262
775
|
const dir = dirArg || '.';
|
|
263
|
-
const pack = options.pack;
|
|
264
776
|
const format = options.format.toLowerCase();
|
|
265
777
|
const isText = format === 'text';
|
|
266
778
|
|
|
779
|
+
// ── Workspace config reader ──────────────────────────────────────────────
|
|
780
|
+
function loadWorkspaceConfig(targetDir) {
|
|
781
|
+
const configPaths = [
|
|
782
|
+
path.join(targetDir, 'korext.json'),
|
|
783
|
+
path.join(targetDir, '.korextrc'),
|
|
784
|
+
path.join(process.cwd(), 'korext.json'),
|
|
785
|
+
path.join(process.cwd(), '.korextrc'),
|
|
786
|
+
];
|
|
787
|
+
// Deduplicate paths (targetDir might equal cwd)
|
|
788
|
+
const seen = new Set();
|
|
789
|
+
for (const p of configPaths) {
|
|
790
|
+
const resolved = path.resolve(p);
|
|
791
|
+
if (seen.has(resolved)) continue;
|
|
792
|
+
seen.add(resolved);
|
|
793
|
+
if (fs.existsSync(resolved)) {
|
|
794
|
+
try {
|
|
795
|
+
const raw = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
|
|
796
|
+
return raw;
|
|
797
|
+
} catch (e) {
|
|
798
|
+
console.warn(`[KOREXT] Invalid config at ${resolved}: ${e.message}`);
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ── Pack resolution priority chain ───────────────────────────────────────
|
|
807
|
+
// 1. --pack flag (highest, explicit)
|
|
808
|
+
// 2. --industry + --region flags (resolved via taxonomy)
|
|
809
|
+
// 3. korext.json targetPacks (workspace config)
|
|
810
|
+
// 4. korext.json industry + region (workspace config, resolved)
|
|
811
|
+
// 5. Default 'web' (fallback)
|
|
812
|
+
let packIds;
|
|
813
|
+
let packSource = 'default';
|
|
814
|
+
|
|
815
|
+
// Helper: try to build taxonomy from cached definitions
|
|
816
|
+
const tryBuildTaxonomy = () => {
|
|
817
|
+
const defs = getRuleDefinitionsCache();
|
|
818
|
+
if (!defs || !defs.packs) return null;
|
|
819
|
+
return buildTaxonomyFromPacks(defs.packs);
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
if (options.pack) {
|
|
823
|
+
// Priority 1: User explicitly passed --pack
|
|
824
|
+
packIds = options.pack.split(',').map(s => s.trim()).filter(Boolean);
|
|
825
|
+
packSource = 'flag';
|
|
826
|
+
} else if (options.industry) {
|
|
827
|
+
// Priority 2: --industry (and optional --region) flags
|
|
828
|
+
const taxonomy = tryBuildTaxonomy();
|
|
829
|
+
if (!taxonomy) {
|
|
830
|
+
if (isText) console.error(chalk.red('\n\u2716 --industry requires cached rule definitions.'));
|
|
831
|
+
if (isText) console.error(chalk.dim(` Run ${chalk.green('korext rules sync')} first to cache pack tags.`));
|
|
832
|
+
process.exit(1);
|
|
833
|
+
}
|
|
834
|
+
packIds = resolvePacksForIndustryRegion(options.industry, options.region || null, taxonomy);
|
|
835
|
+
packSource = 'industry-flag';
|
|
836
|
+
if (packIds.length === 0) {
|
|
837
|
+
if (isText) console.error(chalk.red(`\n\u2716 No packs found for industry '${options.industry}'${options.region ? ` in region '${options.region}'` : ''}.`));
|
|
838
|
+
if (isText) console.error(chalk.dim(` Run ${chalk.green('korext industries')} to see valid industry names.`));
|
|
839
|
+
process.exit(1);
|
|
840
|
+
}
|
|
841
|
+
if (isText) console.log(`[KOREXT] Resolved ${packIds.length} packs for industry: ${options.industry}${options.region ? ` (region: ${options.region})` : ''}`);
|
|
842
|
+
} else {
|
|
843
|
+
// Priority 3-5: workspace config or default
|
|
844
|
+
const config = loadWorkspaceConfig(dirArg || '.');
|
|
845
|
+
if (config) {
|
|
846
|
+
if (Array.isArray(config.targetPacks) && config.targetPacks.length > 0) {
|
|
847
|
+
// Priority 3: korext.json targetPacks
|
|
848
|
+
packIds = config.targetPacks;
|
|
849
|
+
packSource = 'config';
|
|
850
|
+
if (isText) console.log(`[KOREXT] Using packs from korext.json: ${packIds.join(', ')}`);
|
|
851
|
+
} else if (config.industry) {
|
|
852
|
+
// Priority 4: korext.json industry + region
|
|
853
|
+
const taxonomy = tryBuildTaxonomy();
|
|
854
|
+
if (taxonomy) {
|
|
855
|
+
packIds = resolvePacksForIndustryRegion(config.industry, config.region || null, taxonomy);
|
|
856
|
+
packSource = 'config-industry';
|
|
857
|
+
if (packIds.length > 0) {
|
|
858
|
+
if (isText) console.log(`[KOREXT] Resolved ${packIds.length} packs from korext.json industry: ${config.industry}`);
|
|
859
|
+
} else {
|
|
860
|
+
if (isText) console.log(chalk.yellow(`[KOREXT] No packs matched industry '${config.industry}' in korext.json. Falling back to 'web'.`));
|
|
861
|
+
packIds = ['web'];
|
|
862
|
+
}
|
|
863
|
+
} else {
|
|
864
|
+
if (isText) console.log(chalk.yellow('[KOREXT] industry config found but no cached rule definitions. Run korext rules sync. Falling back to web.'));
|
|
865
|
+
packIds = ['web'];
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
packIds = ['web'];
|
|
869
|
+
}
|
|
870
|
+
// Note: config.exclude is acknowledged but not wired up in this version.
|
|
871
|
+
// The enforce command uses hardcoded excludes in findFiles() (node_modules, .git, dist, build, .next).
|
|
872
|
+
} else {
|
|
873
|
+
packIds = ['web'];
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ── Org policy merge (highest priority: mandated packs always included) ──
|
|
878
|
+
const orgToken = getToken();
|
|
879
|
+
if (orgToken && !options.offline) {
|
|
880
|
+
try {
|
|
881
|
+
const policyRes = await fetch(`${API_URL}/api/org/policy/packs`, {
|
|
882
|
+
headers: { Authorization: `Bearer ${orgToken}` },
|
|
883
|
+
});
|
|
884
|
+
if (policyRes.ok) {
|
|
885
|
+
const policyData = await policyRes.json();
|
|
886
|
+
if (policyData.mandated && policyData.mandated.length > 0) {
|
|
887
|
+
const before = packIds.length;
|
|
888
|
+
const mandatedSet = new Set(policyData.mandated);
|
|
889
|
+
// Add mandated packs that are not already selected
|
|
890
|
+
for (const mid of policyData.mandated) {
|
|
891
|
+
if (!packIds.includes(mid)) packIds.push(mid);
|
|
892
|
+
}
|
|
893
|
+
if (isText && packIds.length > before) {
|
|
894
|
+
console.log(chalk.hex('#F27D26')(`[KOREXT] Org policy: ${policyData.mandated.length} mandated pack(s) injected: ${policyData.mandated.join(', ')}`));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
} catch {
|
|
899
|
+
// No org, not authenticated, or server error: continue without org policy
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const packInput = packIds.join(',');
|
|
904
|
+
const pack = packIds.length === 1 ? packIds[0] : packIds;
|
|
905
|
+
|
|
267
906
|
let forceOffline = options.offline;
|
|
268
907
|
let localDefinitions = null;
|
|
269
908
|
|
|
@@ -321,10 +960,12 @@ program
|
|
|
321
960
|
|
|
322
961
|
const report = {
|
|
323
962
|
version,
|
|
324
|
-
packId: pack,
|
|
963
|
+
packId: Array.isArray(pack) ? pack[0] : pack,
|
|
964
|
+
packIds,
|
|
325
965
|
directory: dir,
|
|
326
966
|
summary: { totalFiles: 0, scannedFiles: 0, skippedFiles: 0, errorFiles: 0, critical: 0, high: 0, medium: 0, low: 0, totalViolations: 0 },
|
|
327
|
-
results: []
|
|
967
|
+
results: [],
|
|
968
|
+
bundles: []
|
|
328
969
|
};
|
|
329
970
|
|
|
330
971
|
let files;
|
|
@@ -343,7 +984,7 @@ program
|
|
|
343
984
|
process.exit(0);
|
|
344
985
|
}
|
|
345
986
|
|
|
346
|
-
if (isText) console.log(`Found ${files.length} files. Starting analysis with pack: ${chalk.cyan(
|
|
987
|
+
if (isText) console.log(`Found ${files.length} files. Starting analysis with pack${packIds.length > 1 ? 's' : ''}: ${chalk.cyan(packIds.join(', '))}...\n`);
|
|
347
988
|
|
|
348
989
|
let usedLocalEngine = false;
|
|
349
990
|
|
|
@@ -377,21 +1018,26 @@ program
|
|
|
377
1018
|
} else {
|
|
378
1019
|
// Online mode: try server, fall back to local on failure
|
|
379
1020
|
try {
|
|
1021
|
+
const controller = new AbortController();
|
|
1022
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
380
1023
|
const res = await fetch(`${API_URL}/api/ide/analyze`, {
|
|
381
1024
|
method: 'POST',
|
|
382
1025
|
headers: {
|
|
383
1026
|
'Content-Type': 'application/json',
|
|
384
|
-
...(token && { 'Authorization': `Bearer ${token}` })
|
|
1027
|
+
...(token && { 'Authorization': `Bearer ${token}` }),
|
|
1028
|
+
...(options.sovereigntyRegion && { 'X-Korext-Region': options.sovereigntyRegion })
|
|
385
1029
|
},
|
|
386
1030
|
body: JSON.stringify({
|
|
387
1031
|
fileContent,
|
|
388
1032
|
language,
|
|
389
|
-
fileName: file,
|
|
1033
|
+
fileName: path.basename(file),
|
|
390
1034
|
packId: pack,
|
|
391
|
-
requestSignature:
|
|
1035
|
+
requestSignature: !!options.sign,
|
|
392
1036
|
asyncExplanations: false
|
|
393
|
-
})
|
|
1037
|
+
}),
|
|
1038
|
+
signal: controller.signal
|
|
394
1039
|
});
|
|
1040
|
+
clearTimeout(timeoutId);
|
|
395
1041
|
|
|
396
1042
|
if (!res.ok) {
|
|
397
1043
|
throw new Error(`HTTP ${res.status}`);
|
|
@@ -407,6 +1053,17 @@ program
|
|
|
407
1053
|
|
|
408
1054
|
fileViolations = result.violations || [];
|
|
409
1055
|
|
|
1056
|
+
// Capture proof bundle if present
|
|
1057
|
+
if (result.proofBundle) {
|
|
1058
|
+
report.bundles.push({
|
|
1059
|
+
file: displayPath,
|
|
1060
|
+
bundleId: result.proofBundle.bundleId,
|
|
1061
|
+
decision: result.proofBundle.decision,
|
|
1062
|
+
signed: !!result.proofBundle.hmacSignature,
|
|
1063
|
+
verifyUrl: `https://app.korext.com/verify/${result.proofBundle.bundleId}`,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
410
1067
|
// Cache rule definitions on first successful server response (opportunistic sync)
|
|
411
1068
|
if (!localDefinitions && i === 0) {
|
|
412
1069
|
fetchAndCacheRules().catch(() => {});
|
|
@@ -460,7 +1117,7 @@ program
|
|
|
460
1117
|
}
|
|
461
1118
|
|
|
462
1119
|
if (format === 'json') {
|
|
463
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1120
|
+
console.log(JSON.stringify({ ...report, bundles: report.bundles }, null, 2));
|
|
464
1121
|
} else if (format === 'sarif') {
|
|
465
1122
|
// Build SARIF results
|
|
466
1123
|
const sarifResults = [];
|
|
@@ -489,12 +1146,19 @@ program
|
|
|
489
1146
|
shortDescription: { text: id.replace(/-/g, ' ') }
|
|
490
1147
|
}));
|
|
491
1148
|
|
|
1149
|
+
const sarifProperties = {};
|
|
1150
|
+
if (report.bundles.length > 0) {
|
|
1151
|
+
sarifProperties['korext:bundleIds'] = report.bundles.map(b => b.bundleId);
|
|
1152
|
+
sarifProperties['korext:bundleCount'] = report.bundles.length;
|
|
1153
|
+
sarifProperties['korext:bundlesSigned'] = report.bundles.filter(b => b.signed).length;
|
|
1154
|
+
}
|
|
492
1155
|
const sarif = {
|
|
493
1156
|
version: "2.1.0",
|
|
494
1157
|
$schema: "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
|
|
495
1158
|
runs: [{
|
|
496
1159
|
tool: { driver: { name: "Korext", version, rules } },
|
|
497
|
-
results: sarifResults
|
|
1160
|
+
results: sarifResults,
|
|
1161
|
+
...(Object.keys(sarifProperties).length > 0 && { properties: sarifProperties })
|
|
498
1162
|
}]
|
|
499
1163
|
};
|
|
500
1164
|
console.log(JSON.stringify(sarif, null, 2));
|
|
@@ -531,6 +1195,18 @@ program
|
|
|
531
1195
|
}
|
|
532
1196
|
});
|
|
533
1197
|
}
|
|
1198
|
+
|
|
1199
|
+
// Add proof bundle info
|
|
1200
|
+
if (report.bundles.length > 0) {
|
|
1201
|
+
const signedCount = report.bundles.filter(b => b.signed).length;
|
|
1202
|
+
md += `### Proof Bundles\n\n`;
|
|
1203
|
+
md += `**Generated:** ${report.bundles.length} | **Signed:** ${signedCount}\n\n`;
|
|
1204
|
+
md += `| File | Bundle ID | Decision | Signed | Verify |\n| --- | --- | --- | --- | --- |\n`;
|
|
1205
|
+
for (const b of report.bundles) {
|
|
1206
|
+
md += `| \`${b.file || ''}\` | \`${b.bundleId}\` | ${b.decision} | ${b.signed ? 'Yes' : 'No'} | [Verify](${b.verifyUrl}) |\n`;
|
|
1207
|
+
}
|
|
1208
|
+
md += '\n';
|
|
1209
|
+
}
|
|
534
1210
|
try {
|
|
535
1211
|
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md, 'utf-8');
|
|
536
1212
|
} catch (e) {
|
|
@@ -586,6 +1262,96 @@ rulesCmd
|
|
|
586
1262
|
}
|
|
587
1263
|
});
|
|
588
1264
|
|
|
1265
|
+
// ─── BUNDLE COMMANDS ─────────────────────────────────────────────────────────
|
|
1266
|
+
|
|
1267
|
+
const bundleCmd = program.command('bundle').description('Proof bundle management commands');
|
|
1268
|
+
|
|
1269
|
+
bundleCmd
|
|
1270
|
+
.command('list')
|
|
1271
|
+
.description('List your recent proof bundles (requires authentication)')
|
|
1272
|
+
.action(async () => {
|
|
1273
|
+
const token = getToken();
|
|
1274
|
+
if (!token) {
|
|
1275
|
+
console.log(chalk.red('Not authenticated. Run korext login <token> to sign in.'));
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const spinner = ora('Fetching proof bundles...').start();
|
|
1280
|
+
try {
|
|
1281
|
+
const res = await fetch(`${API_URL}/api/ide/bundles`, {
|
|
1282
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1283
|
+
});
|
|
1284
|
+
if (!res.ok) {
|
|
1285
|
+
spinner.fail(chalk.red(`Server returned ${res.status}: ${res.statusText}`));
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
}
|
|
1288
|
+
const data = await res.json();
|
|
1289
|
+
spinner.stop();
|
|
1290
|
+
|
|
1291
|
+
const bundles = data.bundles || [];
|
|
1292
|
+
if (bundles.length === 0) {
|
|
1293
|
+
console.log(chalk.dim('No proof bundles found. Run korext enforce with --sign to generate bundles.'));
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
console.log(chalk.bold.hex('#F27D26')('\n\u25b2 KOREXT PROOF BUNDLES'));
|
|
1298
|
+
console.log(chalk.dim('=======================================\n'));
|
|
1299
|
+
|
|
1300
|
+
for (const b of bundles) {
|
|
1301
|
+
const signedTag = b.isSigned ? chalk.green(' [SIGNED]') : chalk.dim(' [unsigned]');
|
|
1302
|
+
const decisionTag = b.decision === 'PASS' ? chalk.green(b.decision) : (b.decision === 'BLOCK' ? chalk.red(b.decision) : chalk.yellow(b.decision));
|
|
1303
|
+
console.log(` ${chalk.cyan(b.bundleId)} ${decisionTag}${signedTag}`);
|
|
1304
|
+
console.log(chalk.dim(` Pack: ${b.policyPackId} | Violations: ${b.violationCount} | ${b.ts}`));
|
|
1305
|
+
console.log(chalk.dim(` Verify: ${b.verifyUrl}\n`));
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
console.log(chalk.dim(` Total: ${bundles.length} bundles\n`));
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
spinner.fail(chalk.red(`Failed: ${e.message}`));
|
|
1311
|
+
process.exit(1);
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
bundleCmd
|
|
1316
|
+
.command('verify <bundleId>')
|
|
1317
|
+
.description('Verify a proof bundle by ID')
|
|
1318
|
+
.action(async (bundleId) => {
|
|
1319
|
+
const spinner = ora('Verifying bundle...').start();
|
|
1320
|
+
try {
|
|
1321
|
+
const res = await fetch(`${API_URL}/api/verify/${bundleId}`);
|
|
1322
|
+
if (!res.ok) {
|
|
1323
|
+
spinner.fail(chalk.red(`Bundle not found or verification failed (${res.status}).`));
|
|
1324
|
+
process.exit(1);
|
|
1325
|
+
}
|
|
1326
|
+
const data = await res.json();
|
|
1327
|
+
spinner.stop();
|
|
1328
|
+
|
|
1329
|
+
console.log(chalk.bold.hex('#F27D26')('\n\u25b2 KOREXT BUNDLE VERIFICATION'));
|
|
1330
|
+
console.log(chalk.dim('=======================================\n'));
|
|
1331
|
+
console.log(` Bundle ID: ${chalk.cyan(data.bundleId || bundleId)}`);
|
|
1332
|
+
console.log(` Decision: ${data.decision === 'PASS' ? chalk.green(data.decision) : chalk.red(data.decision)}`);
|
|
1333
|
+
console.log(` Violations: ${data.violationCount ?? 'N/A'}`);
|
|
1334
|
+
console.log(` Policy Pack: ${data.policyPackId || data.policy || 'N/A'}`);
|
|
1335
|
+
console.log(` Signed: ${data.signatureValid ? chalk.green('Valid') : (data.hmacSignature ? chalk.yellow('Unverified') : chalk.dim('No'))}`);
|
|
1336
|
+
console.log(` Timestamp: ${data.ts || 'N/A'}`);
|
|
1337
|
+
console.log('');
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
spinner.fail(chalk.red(`Failed: ${e.message}`));
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
bundleCmd
|
|
1345
|
+
.command('open <bundleId>')
|
|
1346
|
+
.description('Open a proof bundle verification page in your browser')
|
|
1347
|
+
.action(async (bundleId) => {
|
|
1348
|
+
const url = `https://app.korext.com/verify/${bundleId}`;
|
|
1349
|
+
console.log(chalk.dim(`Opening ${url}`));
|
|
1350
|
+
const { exec } = await import('child_process');
|
|
1351
|
+
const cmd = process.platform === 'darwin' ? 'open' : (process.platform === 'win32' ? 'start' : 'xdg-open');
|
|
1352
|
+
exec(`${cmd} "${url}"`);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
589
1355
|
// ─── POLICY COMMANDS ─────────────────────────────────────────────────────────
|
|
590
1356
|
|
|
591
1357
|
const LOCAL_DRAFT_DIR = path.join(process.cwd(), '.korext');
|
|
@@ -1281,7 +2047,7 @@ program
|
|
|
1281
2047
|
'Content-Type': 'application/json',
|
|
1282
2048
|
...(token && { 'Authorization': `Bearer ${token}` })
|
|
1283
2049
|
},
|
|
1284
|
-
body: JSON.stringify({ fileContent, language, fileName: filePath, packId: pack, requestSignature: false, asyncExplanations: false })
|
|
2050
|
+
body: JSON.stringify({ fileContent, language, fileName: path.basename(filePath), packId: pack, requestSignature: false, asyncExplanations: false })
|
|
1285
2051
|
});
|
|
1286
2052
|
if (res.ok) {
|
|
1287
2053
|
const result = await res.json();
|