korext 0.9.6 → 0.9.8
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 +780 -22
- package/package.json +2 -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,22 +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 <packIds>', 'Policy Pack ID(s) to enforce (comma-separated, e.g. web,pci-dss-v1)'
|
|
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 packInput = options.pack;
|
|
264
|
-
// Multi-pack: split on comma, trim whitespace
|
|
265
|
-
const packIds = packInput.split(',').map(p => p.trim()).filter(Boolean);
|
|
266
|
-
const pack = packIds.length === 1 ? packIds[0] : packIds;
|
|
267
776
|
const format = options.format.toLowerCase();
|
|
268
777
|
const isText = format === 'text';
|
|
269
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
|
+
|
|
270
906
|
let forceOffline = options.offline;
|
|
271
907
|
let localDefinitions = null;
|
|
272
908
|
|
|
@@ -328,7 +964,8 @@ program
|
|
|
328
964
|
packIds,
|
|
329
965
|
directory: dir,
|
|
330
966
|
summary: { totalFiles: 0, scannedFiles: 0, skippedFiles: 0, errorFiles: 0, critical: 0, high: 0, medium: 0, low: 0, totalViolations: 0 },
|
|
331
|
-
results: []
|
|
967
|
+
results: [],
|
|
968
|
+
bundles: []
|
|
332
969
|
};
|
|
333
970
|
|
|
334
971
|
let files;
|
|
@@ -387,14 +1024,15 @@ program
|
|
|
387
1024
|
method: 'POST',
|
|
388
1025
|
headers: {
|
|
389
1026
|
'Content-Type': 'application/json',
|
|
390
|
-
...(token && { 'Authorization': `Bearer ${token}` })
|
|
1027
|
+
...(token && { 'Authorization': `Bearer ${token}` }),
|
|
1028
|
+
...(options.sovereigntyRegion && { 'X-Korext-Region': options.sovereigntyRegion })
|
|
391
1029
|
},
|
|
392
1030
|
body: JSON.stringify({
|
|
393
1031
|
fileContent,
|
|
394
1032
|
language,
|
|
395
|
-
fileName: file,
|
|
1033
|
+
fileName: path.basename(file),
|
|
396
1034
|
packId: pack,
|
|
397
|
-
requestSignature:
|
|
1035
|
+
requestSignature: !!options.sign,
|
|
398
1036
|
asyncExplanations: false
|
|
399
1037
|
}),
|
|
400
1038
|
signal: controller.signal
|
|
@@ -415,6 +1053,17 @@ program
|
|
|
415
1053
|
|
|
416
1054
|
fileViolations = result.violations || [];
|
|
417
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
|
+
|
|
418
1067
|
// Cache rule definitions on first successful server response (opportunistic sync)
|
|
419
1068
|
if (!localDefinitions && i === 0) {
|
|
420
1069
|
fetchAndCacheRules().catch(() => {});
|
|
@@ -468,7 +1117,7 @@ program
|
|
|
468
1117
|
}
|
|
469
1118
|
|
|
470
1119
|
if (format === 'json') {
|
|
471
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1120
|
+
console.log(JSON.stringify({ ...report, bundles: report.bundles }, null, 2));
|
|
472
1121
|
} else if (format === 'sarif') {
|
|
473
1122
|
// Build SARIF results
|
|
474
1123
|
const sarifResults = [];
|
|
@@ -497,12 +1146,19 @@ program
|
|
|
497
1146
|
shortDescription: { text: id.replace(/-/g, ' ') }
|
|
498
1147
|
}));
|
|
499
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
|
+
}
|
|
500
1155
|
const sarif = {
|
|
501
1156
|
version: "2.1.0",
|
|
502
1157
|
$schema: "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
|
|
503
1158
|
runs: [{
|
|
504
1159
|
tool: { driver: { name: "Korext", version, rules } },
|
|
505
|
-
results: sarifResults
|
|
1160
|
+
results: sarifResults,
|
|
1161
|
+
...(Object.keys(sarifProperties).length > 0 && { properties: sarifProperties })
|
|
506
1162
|
}]
|
|
507
1163
|
};
|
|
508
1164
|
console.log(JSON.stringify(sarif, null, 2));
|
|
@@ -539,6 +1195,18 @@ program
|
|
|
539
1195
|
}
|
|
540
1196
|
});
|
|
541
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
|
+
}
|
|
542
1210
|
try {
|
|
543
1211
|
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md, 'utf-8');
|
|
544
1212
|
} catch (e) {
|
|
@@ -594,6 +1262,96 @@ rulesCmd
|
|
|
594
1262
|
}
|
|
595
1263
|
});
|
|
596
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
|
+
|
|
597
1355
|
// ─── POLICY COMMANDS ─────────────────────────────────────────────────────────
|
|
598
1356
|
|
|
599
1357
|
const LOCAL_DRAFT_DIR = path.join(process.cwd(), '.korext');
|
|
@@ -1289,7 +2047,7 @@ program
|
|
|
1289
2047
|
'Content-Type': 'application/json',
|
|
1290
2048
|
...(token && { 'Authorization': `Bearer ${token}` })
|
|
1291
2049
|
},
|
|
1292
|
-
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 })
|
|
1293
2051
|
});
|
|
1294
2052
|
if (res.ok) {
|
|
1295
2053
|
const result = await res.json();
|