korext 0.1.0 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+ - **New:** Version bump to match extension releases
5
+ - **Improved:** CLI now reports version 0.3.0
6
+
7
+ ## 0.2.0
8
+ - Initial release
9
+ - `korext login` — browser-based authentication
10
+ - `korext init` — initialize Korext project
11
+ - `korext extract` — extract rules from PDF/Markdown policy documents
12
+ - `korext review` — review extracted rules
13
+ - `korext publish` — activate custom policy packs
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Korext CLI
2
+
3
+ **The Korext Node.js Command Line Interface.**
4
+ Real-time policy enforcement and compliance proof for AI-generated code.
5
+
6
+ [![NPM Version](https://img.shields.io/npm/v/korext)](https://www.npmjs.com/package/korext)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ Install the CLI globally using NPM to access the `korext` command from anywhere:
14
+
15
+ ```bash
16
+ npm install -g korext
17
+ ```
18
+
19
+ ## Authentication
20
+
21
+ Before running analysis, you must authenticate the CLI against your Korext account.
22
+
23
+ ```bash
24
+ # Login via browser (Recommended)
25
+ korext login
26
+
27
+ # Headless CI/CD Login via environment variable
28
+ export KOREXT_API_TOKEN="your_personal_access_token"
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ### `korext enforce [path]`
34
+ Scans a target directory or file and cross-references it against your active Korext Policy Pack in real-time.
35
+
36
+ ```bash
37
+ # Run against the current directory
38
+ korext enforce .
39
+
40
+ # Run against a specific file
41
+ korext enforce ./src/auth.ts
42
+ ```
43
+
44
+ ### `korext status`
45
+ View your current authentication state, active Cloud storage tier, and active Policy Pack ID.
46
+
47
+ ```bash
48
+ korext status
49
+ ```
50
+
51
+ ### `korext packs list`
52
+ Retrieves and lists all available internal Policy Packs available for your organization.
53
+
54
+ ```bash
55
+ korext packs list
56
+ ```
57
+
58
+ ---
59
+
60
+ ## CI/CD Pipeline Integration
61
+
62
+ The Korext CLI is built to run headless inside GitHub Actions, GitLab CI, and isolated Docker environments. When a violation is detected during an enforcement run (`korext enforce`), the CLI intentionally exits with a non-zero code (`exit 1`) to fail the build automatically.
63
+
64
+ **Example GitHub Action:**
65
+ ```yaml
66
+ name: Korext Policy Pipeline
67
+ on: [push, pull_request]
68
+
69
+ jobs:
70
+ audit:
71
+ runs-on: ubuntu-latest
72
+ steps:
73
+ - uses: actions/checkout@v4
74
+ - uses: actions/setup-node@v4
75
+ with:
76
+ node-version: '20'
77
+
78
+ - name: Install Korext globally
79
+ run: npm install -g korext
80
+
81
+ - name: Korext Enforce
82
+ env:
83
+ KOREXT_API_TOKEN: ${{ secrets.KOREXT_API_TOKEN }}
84
+ run: korext enforce .
85
+ ```
86
+
87
+ ## Need Help?
88
+ Visit the official documentation at [Korext.com](https://www.korext.com) or reach out to your Account Executive for Enterprise Support.
package/bin/korext.js CHANGED
@@ -159,49 +159,62 @@ program
159
159
  .command('enforce [dir]')
160
160
  .description('Statically analyze files in a directory against Korext policies')
161
161
  .option('-p, --pack <packId>', 'Policy Pack ID to enforce', 'web')
162
+ .option('-f, --format <format>', 'Output format (text, json, sarif)', 'text')
162
163
  .action(async (dirArg, options) => {
163
164
  const dir = dirArg || '.';
164
165
  const pack = options.pack;
165
-
166
- console.log(`\n${chalk.bold.hex('#F27D26')('▲ KOREXT CLI ENFORCEMENT ENGINE')} v${version}`);
167
- console.log(chalk.dim('======================================='));
166
+ const format = options.format.toLowerCase();
167
+ const isText = format === 'text';
168
+
169
+ if (isText) {
170
+ console.log(`\n${chalk.bold.hex('#F27D26')('▲ KOREXT CLI ENFORCEMENT ENGINE')} v${version}`);
171
+ console.log(chalk.dim('======================================='));
172
+ }
168
173
 
169
174
  const token = getToken();
170
- if (!token) {
175
+ if (!token && isText) {
171
176
  console.log(chalk.yellow('\n⚠ Warning: No authentication token found.'));
172
177
  console.log(chalk.dim('Anonymous evaluation runs are limited to 20 requests per hour per IP.'));
173
178
  console.log(chalk.dim(`Run ${chalk.green('korext login')} to authenticate for unlimited CI/CD analytics.\n`));
174
179
  }
175
180
 
176
- const spinner = ora(`Scanning directory '${dir}' for supported files...`).start();
181
+ const report = {
182
+ version,
183
+ packId: pack,
184
+ directory: dir,
185
+ summary: { totalFiles: 0, scannedFiles: 0, skippedFiles: 0, errorFiles: 0, critical: 0, high: 0, medium: 0, low: 0, totalViolations: 0 },
186
+ results: []
187
+ };
188
+
177
189
  let files;
178
190
  try {
179
191
  files = findFiles(dir);
180
192
  } catch (e) {
181
- spinner.fail(chalk.red(`Error reading directory: ${e.message}`));
193
+ if (isText) console.error(chalk.red(`Error reading directory: ${e.message}`));
182
194
  process.exit(1);
183
195
  }
184
196
 
197
+ report.summary.totalFiles = files.length;
198
+
185
199
  if (files.length === 0) {
186
- spinner.succeed('Done. No supported files found.');
200
+ if (isText) console.log('Done. No supported files found.');
201
+ else console.log(JSON.stringify(report, null, 2));
187
202
  process.exit(0);
188
203
  }
189
204
 
190
- spinner.succeed(`Found ${files.length} files. Starting analysis with pack: ${chalk.cyan(pack)}...\n`);
191
-
192
- let totalViolations = 0;
193
- let criticalCount = 0;
194
- let highCount = 0;
205
+ if (isText) console.log(`Found ${files.length} files. Starting analysis with pack: ${chalk.cyan(pack)}...\n`);
195
206
 
196
207
  for (let i = 0; i < files.length; i++) {
197
208
  const file = files[i];
198
209
  const displayPath = path.relative(process.cwd(), file);
199
210
 
200
- const fileSpinner = ora(`Analyzing ${displayPath} (${i + 1}/${files.length})...`).start();
211
+ let fileSpinner = null;
212
+ if (isText) fileSpinner = ora(`Analyzing ${displayPath} (${i + 1}/${files.length})...`).start();
201
213
 
202
214
  const fileContent = fs.readFileSync(file, 'utf-8');
203
215
  if (fileContent.length > 500000) {
204
- fileSpinner.info(chalk.yellow(`Skipped ${displayPath} (File too large)`));
216
+ if (fileSpinner) fileSpinner.info(chalk.yellow(`Skipped ${displayPath} (File too large)`));
217
+ report.summary.skippedFiles++;
205
218
  continue;
206
219
  }
207
220
 
@@ -225,54 +238,756 @@ program
225
238
  });
226
239
 
227
240
  if (!res.ok) {
228
- fileSpinner.fail(chalk.red(`Server error analyzing ${displayPath}: HTTP ${res.status}`));
241
+ if (fileSpinner) fileSpinner.fail(chalk.red(`Server error analyzing ${displayPath}: HTTP ${res.status}`));
242
+ report.summary.errorFiles++;
229
243
  continue;
230
244
  }
231
245
 
232
246
  const result = await res.json();
233
247
 
234
248
  if (result.skipped) {
235
- fileSpinner.info(chalk.yellow(`Skipped ${displayPath}: ${result.reason}`));
249
+ if (fileSpinner) fileSpinner.info(chalk.yellow(`Skipped ${displayPath}: ${result.reason}`));
250
+ report.summary.skippedFiles++;
236
251
  continue;
237
252
  }
238
253
 
239
- if (result.summary && result.summary.total > 0) {
240
- fileSpinner.stop();
241
- console.log(`\n📄 ${chalk.bold.underline(displayPath)}`);
254
+ report.summary.scannedFiles++;
255
+ const fileViolations = result.violations || [];
256
+ report.summary.totalViolations += fileViolations.length;
257
+
258
+ if (fileViolations.length > 0) {
259
+ if (fileSpinner) fileSpinner.stop();
260
+ if (isText) console.log(`\n📄 ${chalk.bold.underline(displayPath)}`);
242
261
 
243
- for (const v of result.violations) {
244
- totalViolations++;
262
+ const storedV = [];
263
+ for (const v of fileViolations) {
245
264
  let severityTag = chalk.blue('LOW');
246
- if (v.severity === 'critical') { severityTag = chalk.bgRed.white.bold(' CRITICAL '); criticalCount++; }
247
- else if (v.severity === 'high') { severityTag = chalk.red.bold('HIGH'); highCount++; }
248
- else if (v.severity === 'medium') { severityTag = chalk.yellow.bold('MED'); }
265
+ if (v.severity === 'critical') { severityTag = chalk.bgRed.white.bold(' CRITICAL '); report.summary.critical++; }
266
+ else if (v.severity === 'high') { severityTag = chalk.red.bold('HIGH'); report.summary.high++; }
267
+ else if (v.severity === 'medium') { severityTag = chalk.yellow.bold('MED'); report.summary.medium++; }
268
+ else { report.summary.low++; }
249
269
 
250
- console.log(` ${chalk.dim(v.line + ':' + (v.column || 0))} ${severityTag} ${v.ruleName}`);
251
- console.log(` ${chalk.dim('')} ${chalk.italic(v.explanation)} `);
270
+ if (isText) {
271
+ console.log(` ${chalk.dim(v.line + ':' + (v.column || 0))} ${severityTag} ${v.ruleName}`);
272
+ console.log(` ${chalk.dim('↳')} ${chalk.italic(v.explanation)} `);
273
+ }
274
+ storedV.push(v);
252
275
  }
276
+ report.results.push({ file: displayPath, violations: storedV });
253
277
  } else {
254
- fileSpinner.stop();
278
+ if (fileSpinner) fileSpinner.stop();
279
+ report.results.push({ file: displayPath, violations: [] });
255
280
  }
256
281
  } catch (e) {
257
- fileSpinner.fail(chalk.red(`Failed to analyze ${displayPath}: ${e.message}`));
282
+ if (fileSpinner) fileSpinner.fail(chalk.red(`Failed to analyze ${displayPath}: ${e.message}`));
283
+ report.summary.errorFiles++;
258
284
  }
259
285
  }
260
286
 
261
- console.log('\n' + chalk.dim('======================================='));
262
-
263
- if (totalViolations === 0) {
264
- console.log(chalk.green.bold('✔ Success! Found 0 policy violations. Your code is clean.\n'));
287
+ if (format === 'json') {
288
+ console.log(JSON.stringify(report, null, 2));
289
+ } else if (format === 'sarif') {
290
+ const sarif = {
291
+ version: "2.1.0",
292
+ $schema: "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
293
+ runs: [{
294
+ tool: { driver: { name: "Korext", version } },
295
+ results: []
296
+ }]
297
+ };
298
+ for (const res of report.results) {
299
+ for (const v of res.violations) {
300
+ sarif.runs[0].results.push({
301
+ ruleId: v.ruleName,
302
+ level: v.severity === 'critical' || v.severity === 'high' ? "error" : v.severity === 'medium' ? "warning" : "note",
303
+ message: { text: v.explanation },
304
+ locations: [{
305
+ physicalLocation: {
306
+ artifactLocation: { uri: res.file },
307
+ region: { startLine: v.line, startColumn: v.column || 1 }
308
+ }
309
+ }]
310
+ });
311
+ }
312
+ }
313
+ console.log(JSON.stringify(sarif, null, 2));
314
+ } else {
315
+ console.log('\n' + chalk.dim('======================================='));
316
+ if (report.summary.totalViolations === 0 && report.summary.errorFiles === 0) {
317
+ console.log(chalk.green.bold('✔ Success! Found 0 policy violations. Your code is clean.\n'));
318
+ } else {
319
+ console.log(chalk.red.bold(`✖ Analysis complete. Found ${report.summary.totalViolations} total policy violations.`));
320
+ }
321
+ }
322
+
323
+ if (process.env.GITHUB_STEP_SUMMARY) {
324
+ let md = `## 🛡️ Korext Code Governance Report\n\n`;
325
+ md += `**Scanned:** ${report.summary.scannedFiles} files | **Violations:** ${report.summary.totalViolations}\n\n`;
326
+ md += `| Severity | Count |\n| --- | --- |\n`;
327
+ md += `| 🔴 CRITICAL | ${report.summary.critical} |\n`;
328
+ md += `| 🟠 HIGH | ${report.summary.high} |\n`;
329
+ md += `| 🟡 MEDIUM | ${report.summary.medium} |\n`;
330
+ md += `| 🔵 LOW | ${report.summary.low} |\n\n`;
331
+
332
+ if (report.summary.totalViolations > 0) {
333
+ md += `### Violations Found:\n\n`;
334
+ report.results.forEach(res => {
335
+ if (res.violations.length > 0) {
336
+ md += `**\`${res.file}\`**\n`;
337
+ res.violations.forEach(v => {
338
+ md += `- \`Line ${v.line}\` **[${v.severity.toUpperCase()}]** ${v.ruleName}: *${v.explanation}*\n`;
339
+ });
340
+ md += '\n';
341
+ }
342
+ });
343
+ }
344
+ try {
345
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md, 'utf-8');
346
+ } catch (e) {
347
+ // fail silently if runner volume is unmountable
348
+ }
349
+ }
350
+
351
+ if (report.summary.errorFiles > 0) process.exit(2);
352
+ if (report.summary.critical > 0 || report.summary.high > 0) {
353
+ if (isText) console.log(chalk.red('Quality Gate Failed: Critical or High severity violations detected.\n'));
354
+ process.exit(1);
355
+ } else if (report.summary.totalViolations > 0) {
356
+ if (isText) console.log(chalk.yellow('Warnings present, but no critical/high gates triggered. Passing build.\n'));
265
357
  process.exit(0);
358
+ }
359
+ process.exit(0);
360
+ });
361
+
362
+ // ─── POLICY COMMANDS ─────────────────────────────────────────────────────────
363
+
364
+ const LOCAL_DRAFT_DIR = path.join(process.cwd(), '.korext');
365
+ const APPROVED_PACK_FILE = path.join(LOCAL_DRAFT_DIR, 'approved-pack.json');
366
+ const REVIEW_PORT = 3847;
367
+
368
+ function ensureKorextDir() {
369
+ if (!fs.existsSync(LOCAL_DRAFT_DIR)) {
370
+ fs.mkdirSync(LOCAL_DRAFT_DIR, { mode: 0o700, recursive: true });
371
+ }
372
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
373
+ if (fs.existsSync(gitignorePath)) {
374
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
375
+ if (!content.includes('.korext/')) {
376
+ fs.appendFileSync(gitignorePath, '\n# Korext policy drafts (may contain sensitive extracted policy text)\n.korext/\n');
377
+ }
378
+ } else {
379
+ fs.writeFileSync(gitignorePath, '# Korext policy drafts\n.korext/\n');
380
+ }
381
+ }
382
+
383
+ function getLatestDraft() {
384
+ if (!fs.existsSync(LOCAL_DRAFT_DIR)) {
385
+ console.error(chalk.red('✖ No draft found. Run: korext policy init --file <path> --name <name>'));
386
+ process.exit(1);
387
+ }
388
+ const files = fs.readdirSync(LOCAL_DRAFT_DIR)
389
+ .filter(f => f.startsWith('draft-') && f.endsWith('.json') && f !== 'draft-pack.json')
390
+ .sort()
391
+ .reverse();
392
+ // Fallback to legacy draft-pack.json
393
+ if (files.length === 0 && fs.existsSync(path.join(LOCAL_DRAFT_DIR, 'draft-pack.json'))) {
394
+ return JSON.parse(fs.readFileSync(path.join(LOCAL_DRAFT_DIR, 'draft-pack.json'), 'utf-8'));
395
+ }
396
+ if (files.length === 0) {
397
+ console.error(chalk.red('✖ No draft found. Run: korext policy init --file <path> --name <name>'));
398
+ process.exit(1);
399
+ }
400
+ const latest = path.join(LOCAL_DRAFT_DIR, files[0]);
401
+ return { ...JSON.parse(fs.readFileSync(latest, 'utf-8')), _draftPath: latest };
402
+ }
403
+
404
+ function saveLatestDraft(draft) {
405
+ const draftPath = draft._draftPath || path.join(LOCAL_DRAFT_DIR, 'draft-pack.json');
406
+ const toSave = { ...draft };
407
+ delete toSave._draftPath;
408
+ fs.writeFileSync(draftPath, JSON.stringify(toSave, null, 2), { mode: 0o600 });
409
+ }
410
+
411
+ function parseMarkdownSections(content) {
412
+ // Strip YAML front matter
413
+ const stripped = content.replace(/^---[\s\S]*?---\s*\n/, '');
414
+ const lines = stripped.split('\n');
415
+ const sections = [];
416
+ let currentHeading = 'Document Start';
417
+ let currentContent = [];
418
+
419
+ for (const line of lines) {
420
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
421
+ if (headingMatch) {
422
+ if (currentContent.length > 0) {
423
+ sections.push({ heading: currentHeading, content: currentContent.join('\n').trim() });
424
+ }
425
+ currentHeading = headingMatch[2].trim();
426
+ currentContent = [];
266
427
  } else {
267
- console.log(chalk.red.bold(`✖ Analysis complete. Found ${totalViolations} total policy violations.`));
268
- if (criticalCount > 0 || highCount > 0) {
269
- console.log(chalk.red('Quality Gate Failed: Critical or High severity violations detected.\n'));
428
+ currentContent.push(line);
429
+ }
430
+ }
431
+ if (currentContent.length > 0) {
432
+ sections.push({ heading: currentHeading, content: currentContent.join('\n').trim() });
433
+ }
434
+ // Filter out empty sections
435
+ return sections.filter(s => s.content.length > 0);
436
+ }
437
+
438
+ const policyCmd = program.command('policy').description('Local policy document processing commands');
439
+
440
+ // ── korext policy init ─────────────────────────────────────────────────────
441
+ policyCmd
442
+ .command('init')
443
+ .description('Process a policy document locally (zero network calls)')
444
+ .requiredOption('--file <path>', 'Path to policy document (.md, .pdf)')
445
+ .requiredOption('--name <name>', 'Name for this policy pack')
446
+ .option('--description <text>', 'Description for this policy pack', '')
447
+ .action((options) => {
448
+ const filePath = path.resolve(options.file);
449
+
450
+ if (!fs.existsSync(filePath)) {
451
+ console.error(chalk.red(`File not found at ${filePath}.\nCheck the path and try again.`));
452
+ process.exit(1);
453
+ }
454
+
455
+ const ext = path.extname(filePath).toLowerCase();
456
+ if (!['.md', '.markdown', '.mdx', '.pdf'].includes(ext)) {
457
+ console.error(chalk.red(`Only .pdf and .md files are supported. Got ${ext}.`));
458
+ process.exit(1);
459
+ }
460
+
461
+ const stats = fs.statSync(filePath);
462
+ if (stats.size === 0) {
463
+ console.error(chalk.red('The file appears to be empty.'));
464
+ process.exit(1);
465
+ }
466
+ if (stats.size > 50 * 1024 * 1024) {
467
+ console.error(chalk.red('File is too large. Maximum size is 50MB.'));
468
+ process.exit(1);
469
+ }
470
+
471
+ ensureKorextDir();
472
+
473
+ let sections = [];
474
+ let wordCount = 0;
475
+ const isPdf = ext === '.pdf';
476
+
477
+ if (!isPdf) {
478
+ const raw = fs.readFileSync(filePath, 'utf-8');
479
+ sections = parseMarkdownSections(raw);
480
+ const allText = sections.map(s => s.content).join(' ');
481
+ wordCount = allText.split(/\s+/).filter(w => w.length > 0).length;
482
+ if (wordCount < 20) {
483
+ console.error(chalk.red('The file appears to be empty.'));
484
+ process.exit(1);
485
+ }
486
+ }
487
+
488
+ const timestamp = Date.now();
489
+ const draftFilename = `draft-${timestamp}.json`;
490
+ const draftPath = path.join(LOCAL_DRAFT_DIR, draftFilename);
491
+
492
+ const draft = {
493
+ packName: options.name,
494
+ packDescription: options.description,
495
+ sourceFile: path.basename(filePath),
496
+ fileType: isPdf ? 'pdf' : 'markdown',
497
+ wordCount,
498
+ extractedAt: new Date().toISOString(),
499
+ sections,
500
+ // Internal metadata for extract step
501
+ _sourceFullPath: filePath,
502
+ _sourceExt: ext,
503
+ rules: [],
504
+ status: 'init'
505
+ };
506
+
507
+ fs.writeFileSync(draftPath, JSON.stringify(draft, null, 2), { mode: 0o600 });
508
+
509
+ console.log(chalk.green('\nDocument processed locally.'));
510
+ console.log(chalk.green(`Nothing was sent to Korext servers.\n`));
511
+ console.log(` ${chalk.bold(sections.length)} sections extracted.`);
512
+ console.log(` ${chalk.bold(wordCount)} words found.`);
513
+ console.log(` Ready to extract rules.\n`);
514
+ console.log(` Run: ${chalk.cyan('korext policy extract')}`);
515
+ });
516
+
517
+ // ── korext policy extract ──────────────────────────────────────────────────
518
+ policyCmd
519
+ .command('extract')
520
+ .description('Extract enforceable coding rules from your policy document')
521
+ .option('--airgap', 'Airgap mode: skip API call, prepare manual template', false)
522
+ .action(async (options) => {
523
+ const draft = getLatestDraft();
524
+ const token = getToken();
525
+
526
+ if (options.airgap) {
527
+ draft.rules = [];
528
+ draft.status = 'airgap-review';
529
+ const templateRule = {
530
+ ruleText: 'Paste the exact text from your policy document here',
531
+ plainEnglish: 'What developers should avoid or do',
532
+ severity: 'high',
533
+ category: 'Security',
534
+ codePatterns: ['pattern_to_detect_violation'],
535
+ sourceSection: 'Section name',
536
+ regulatoryBasis: null
537
+ };
538
+ draft.templateRule = templateRule;
539
+ saveLatestDraft(draft);
540
+ console.log(chalk.yellow('⚡ Airgap mode — no network calls made'));
541
+ console.log(chalk.dim('\nNext: korext policy review'));
542
+ return;
543
+ }
544
+
545
+ if (!token) {
546
+ console.error(chalk.red('✖ Authentication required for rule extraction.'));
547
+ console.error(chalk.dim(' Run: korext login <token>'));
548
+ console.error(chalk.dim(' Or use: korext policy extract --airgap for offline mode'));
549
+ process.exit(1);
550
+ }
551
+
552
+ const spinner = ora('Extracting rules from your policy document...').start();
553
+
554
+ // Build text from sections (ONLY text, never raw file)
555
+ let textPayload = null;
556
+ if (draft.sections && draft.sections.length > 0) {
557
+ textPayload = draft.sections.map(s => `[Section: ${s.heading}]\n${s.content}`).join('\n\n');
558
+ } else if (draft.extractedText) {
559
+ // Legacy fallback
560
+ textPayload = draft.extractedText;
561
+ }
562
+
563
+ if (!textPayload && draft.fileType === 'pdf') {
564
+ spinner.info('PDF detected — uploading for server-side text extraction only');
565
+ textPayload = null;
566
+ }
567
+
568
+ try {
569
+ const body = JSON.stringify({
570
+ packName: draft.packName,
571
+ packDescription: draft.packDescription || '',
572
+ extractedText: textPayload,
573
+ mode: 'extract-only'
574
+ // NOTE: No sourceFilename, no file path, no raw file
575
+ });
576
+
577
+ const res = await fetch(`${API_URL}/api/packs/extract`, {
578
+ method: 'POST',
579
+ headers: {
580
+ 'Content-Type': 'application/json',
581
+ 'Authorization': `Bearer ${token}`
582
+ },
583
+ body
584
+ });
585
+
586
+ if (!res.ok) {
587
+ const err = await res.json().catch(() => ({}));
588
+ spinner.fail(chalk.red(`Extraction failed: ${err.error || `HTTP ${res.status}`}`));
270
589
  process.exit(1);
271
- } else {
272
- console.log(chalk.yellow('Warnings present, but no critical/high gates triggered. Passing build.\n'));
273
- process.exit(0);
274
590
  }
591
+
592
+ const data = await res.json();
593
+ draft.rules = data.rules || [];
594
+ draft.status = 'pending-review';
595
+ saveLatestDraft(draft);
596
+
597
+ spinner.succeed(chalk.green(`${draft.rules.length} rules identified.`));
598
+ console.log(`Run: ${chalk.cyan('korext policy review')}`);
599
+ } catch (e) {
600
+ spinner.fail(chalk.red(`Network error: ${e.message}`));
601
+ process.exit(1);
275
602
  }
276
603
  });
277
604
 
605
+ // ── korext policy review ───────────────────────────────────────────────────
606
+ policyCmd
607
+ .command('review')
608
+ .description('Open a local browser UI to review extracted rules (zero network calls)')
609
+ .action(async () => {
610
+ const draft = getLatestDraft();
611
+
612
+ if (!draft.rules || draft.rules.length === 0) {
613
+ console.error(chalk.red('No rules found.\nRun: korext policy extract first.'));
614
+ process.exit(1);
615
+ }
616
+
617
+ // Assign IDs if missing
618
+ draft.rules.forEach((r, i) => {
619
+ if (!r.id) r.id = `rule-${i}`;
620
+ if (!r.status) r.status = 'pending';
621
+ });
622
+ saveLatestDraft(draft);
623
+
624
+ const http = await import('http');
625
+
626
+ const reviewHTML = generateReviewHTML(draft);
627
+
628
+ const server = http.createServer((req, res) => {
629
+ if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
630
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
631
+ res.end(reviewHTML);
632
+ return;
633
+ }
634
+
635
+ if (req.method === 'GET' && req.url === '/api/rules') {
636
+ res.writeHead(200, { 'Content-Type': 'application/json' });
637
+ res.end(JSON.stringify({ packName: draft.packName, rules: draft.rules }));
638
+ return;
639
+ }
640
+
641
+ if (req.method === 'POST' && req.url === '/api/rules/update') {
642
+ let body = '';
643
+ req.on('data', c => body += c);
644
+ req.on('end', () => {
645
+ try {
646
+ const { id, status, plainEnglish, severity } = JSON.parse(body);
647
+ const rule = draft.rules.find(r => r.id === id);
648
+ if (rule) {
649
+ if (status) rule.status = status;
650
+ if (plainEnglish !== undefined) { rule.plainEnglish = plainEnglish; rule.editedByUser = true; }
651
+ if (severity) rule.severity = severity;
652
+ saveLatestDraft(draft);
653
+ }
654
+ res.writeHead(200, { 'Content-Type': 'application/json' });
655
+ res.end(JSON.stringify({ ok: true }));
656
+ } catch (e) {
657
+ res.writeHead(400, { 'Content-Type': 'application/json' });
658
+ res.end(JSON.stringify({ error: e.message }));
659
+ }
660
+ });
661
+ return;
662
+ }
663
+
664
+ if (req.method === 'POST' && req.url === '/api/finish') {
665
+ const approved = draft.rules.filter(r => r.status === 'approved');
666
+ if (approved.length === 0) {
667
+ res.writeHead(400, { 'Content-Type': 'application/json' });
668
+ res.end(JSON.stringify({ error: 'No rules approved' }));
669
+ return;
670
+ }
671
+ // Save approved-pack.json
672
+ const approvedPack = {
673
+ packName: draft.packName,
674
+ packDescription: draft.packDescription || '',
675
+ extractedAt: new Date().toISOString(),
676
+ rules: approved
677
+ };
678
+ fs.writeFileSync(APPROVED_PACK_FILE, JSON.stringify(approvedPack, null, 2), { mode: 0o600 });
679
+ res.writeHead(200, { 'Content-Type': 'application/json' });
680
+ res.end(JSON.stringify({ ok: true, approved: approved.length }));
681
+ // Shut down the review server after a brief delay
682
+ setTimeout(() => {
683
+ server.close();
684
+ console.log(chalk.green(`\n${approved.length} rules approved.`));
685
+ console.log(`Run: ${chalk.cyan('korext policy publish --org <org-id>')}`);
686
+ process.exit(0);
687
+ }, 500);
688
+ return;
689
+ }
690
+
691
+ res.writeHead(404);
692
+ res.end('Not found');
693
+ });
694
+
695
+ server.listen(REVIEW_PORT, '127.0.0.1', async () => {
696
+ const url = `http://localhost:${REVIEW_PORT}`;
697
+ console.log(chalk.bold.hex('#F27D26')('\n▲ KOREXT POLICY REVIEW'));
698
+ console.log(chalk.dim('======================================='));
699
+ console.log(`Pack: ${chalk.cyan(draft.packName)}`);
700
+ console.log(`Rules: ${chalk.bold(draft.rules.length)} total`);
701
+ console.log(`\nOpening review interface at: ${chalk.cyan(url)}\n`);
702
+ console.log(chalk.dim('All review happens locally. Zero network calls.\nPress Ctrl+C to cancel.\n'));
703
+
704
+ // Auto-open browser
705
+ const platform = os.platform();
706
+ try {
707
+ const { execSync } = await import('child_process');
708
+ if (platform === 'darwin') execSync(`open ${url}`);
709
+ else if (platform === 'linux') execSync(`xdg-open ${url}`);
710
+ else if (platform === 'win32') execSync(`start ${url}`);
711
+ } catch (_) {
712
+ console.log(`Open your browser at: ${chalk.cyan(url)}`);
713
+ }
714
+ });
715
+
716
+ server.on('error', (e) => {
717
+ if (e.code === 'EADDRINUSE') {
718
+ console.error(chalk.red(`Port ${REVIEW_PORT} is already in use. Stop the other process and try again.`));
719
+ process.exit(1);
720
+ }
721
+ throw e;
722
+ });
723
+ });
724
+
725
+ function generateReviewHTML(draft) {
726
+ const rulesJSON = JSON.stringify(draft.rules).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
727
+ const packName = (draft.packName || '').replace(/"/g, '&quot;').replace(/</g, '&lt;');
728
+ return `<!DOCTYPE html>
729
+ <html lang="en">
730
+ <head>
731
+ <meta charset="UTF-8">
732
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
733
+ <title>Korext Policy Review — ${packName}</title>
734
+ <style>
735
+ :root {
736
+ --bg: #0d1117; --surface: #161b22; --border: #30363d;
737
+ --text: #e6edf3; --dim: #8b949e; --accent: #F27D26;
738
+ --green: #3fb950; --red: #f85149; --yellow: #d29922; --blue: #58a6ff;
739
+ --card-radius: 12px;
740
+ }
741
+ * { margin: 0; padding: 0; box-sizing: border-box; }
742
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
743
+ .header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 20px 24px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 100; }
744
+ .header h1 { font-size: 18px; color: var(--accent); }
745
+ .header .pack-name { color: var(--dim); font-size: 14px; margin-top: 4px; }
746
+ .counters { display: flex; gap: 16px; font-size: 14px; font-weight: 600; }
747
+ .counters .approved { color: var(--green); } .counters .rejected { color: var(--red); } .counters .remaining { color: var(--yellow); }
748
+ .container { max-width: 800px; margin: 24px auto; padding: 0 16px; }
749
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--card-radius); padding: 20px; margin-bottom: 16px; transition: border-color 0.3s, opacity 0.3s; }
750
+ .card.card-approved { border-color: var(--green); background: #0d1f0d; }
751
+ .card.card-rejected { border-color: var(--border); opacity: 0.5; }
752
+ .card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
753
+ .badge { padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
754
+ .badge-critical { background: #f85149; color: #fff; } .badge-high { background: #da3633; color: #fff; }
755
+ .badge-medium { background: #d29922; color: #000; } .badge-low { background: #388bfd26; color: var(--blue); }
756
+ .card-category { color: var(--dim); font-size: 12px; }
757
+ .rule-text { color: var(--dim); font-style: italic; font-size: 13px; margin-bottom: 8px; border-left: 3px solid var(--border); padding-left: 12px; }
758
+ .plain-english { font-size: 15px; font-weight: 500; margin-bottom: 8px; }
759
+ .source-section { color: var(--dim); font-size: 12px; margin-bottom: 12px; }
760
+ .actions { display: flex; gap: 8px; flex-wrap: wrap; }
761
+ .btn { padding: 8px 18px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); color: var(--text); cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s; }
762
+ .btn:hover { filter: brightness(1.2); }
763
+ .btn-approve { background: #238636; border-color: #2ea043; color: #fff; }
764
+ .btn-reject { background: #21262d; border-color: var(--border); color: var(--red); }
765
+ .btn-edit { background: #21262d; border-color: var(--border); color: var(--blue); }
766
+ .btn-undo { background: #21262d; font-size: 12px; padding: 6px 12px; }
767
+ .edit-area { margin-top: 12px; display: none; }
768
+ .edit-area.open { display: block; }
769
+ .edit-area textarea { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 10px; font-size: 14px; resize: vertical; min-height: 60px; }
770
+ .edit-area select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; margin-top: 8px; font-size: 13px; }
771
+ .edit-area .edit-actions { display: flex; gap: 8px; margin-top: 10px; }
772
+ .finish-bar { position: fixed; bottom: 0; left: 0; right: 0; background: var(--surface); border-top: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; justify-content: center; gap: 16px; z-index: 100; }
773
+ .btn-finish { background: linear-gradient(135deg, #F27D26, #e85d04); color: #fff; border: none; padding: 12px 32px; font-size: 15px; font-weight: 700; border-radius: 10px; cursor: pointer; transition: transform 0.15s; }
774
+ .btn-finish:hover { transform: scale(1.03); }
775
+ .btn-finish:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
776
+ .done-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.85); z-index: 200; align-items: center; justify-content: center; flex-direction: column; }
777
+ .done-overlay.show { display: flex; }
778
+ .done-overlay h2 { font-size: 28px; color: var(--green); margin-bottom: 8px; }
779
+ .done-overlay p { color: var(--dim); font-size: 16px; }
780
+ .privacy-note { text-align: center; color: var(--dim); font-size: 12px; padding: 8px; margin-bottom: 80px; }
781
+ @media (max-width: 600px) {
782
+ .header { flex-direction: column; gap: 10px; align-items: flex-start; }
783
+ .actions { flex-direction: column; }
784
+ .btn { width: 100%; text-align: center; }
785
+ }
786
+ </style>
787
+ </head>
788
+ <body>
789
+ <div class="header">
790
+ <div>
791
+ <h1>▲ Korext Policy Review</h1>
792
+ <div class="pack-name">${packName}</div>
793
+ </div>
794
+ <div class="counters">
795
+ <span class="approved" id="c-approved">0 approved</span>
796
+ <span class="rejected" id="c-rejected">0 rejected</span>
797
+ <span class="remaining" id="c-remaining">0 remaining</span>
798
+ </div>
799
+ </div>
800
+ <div class="container" id="rules-container"></div>
801
+ <p class="privacy-note">🔒 All review is happening locally. Zero network requests are made during this session.</p>
802
+ <div class="finish-bar">
803
+ <button class="btn-finish" id="finish-btn" disabled onclick="finishReview()">Finish Review</button>
804
+ </div>
805
+ <div class="done-overlay" id="done-overlay">
806
+ <h2>✔ Review Complete</h2>
807
+ <p id="done-msg"></p>
808
+ <p style="margin-top:12px;color:#8b949e;">You can close this tab now.</p>
809
+ </div>
810
+ <script>
811
+ const rules = ${rulesJSON};
812
+ const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
813
+
814
+ function updateCounters() {
815
+ const approved = rules.filter(r => r.status === 'approved').length;
816
+ const rejected = rules.filter(r => r.status === 'rejected').length;
817
+ const remaining = rules.length - approved - rejected;
818
+ document.getElementById('c-approved').textContent = approved + ' approved';
819
+ document.getElementById('c-rejected').textContent = rejected + ' rejected';
820
+ document.getElementById('c-remaining').textContent = remaining + ' remaining';
821
+ document.getElementById('finish-btn').disabled = approved === 0;
822
+ }
823
+
824
+ function renderCards() {
825
+ const container = document.getElementById('rules-container');
826
+ container.innerHTML = '';
827
+ rules.forEach((rule, i) => {
828
+ const card = document.createElement('div');
829
+ card.className = 'card' + (rule.status === 'approved' ? ' card-approved' : '') + (rule.status === 'rejected' ? ' card-rejected' : '');
830
+ card.id = 'card-' + i;
831
+ card.innerHTML = \`
832
+ <div class="card-header">
833
+ <span class="badge badge-\${rule.severity}">\${rule.severity}</span>
834
+ <span class="card-category">\${rule.category || ''}</span>
835
+ </div>
836
+ <div class="plain-english">\${esc(rule.plainEnglish)}</div>
837
+ <div class="rule-text">\${esc(rule.ruleText)}</div>
838
+ \${rule.sourceSection ? '<div class="source-section">📄 ' + esc(rule.sourceSection) + '</div>' : ''}
839
+ <div class="actions" id="actions-\${i}">
840
+ \${rule.status === 'approved' ? '<button class="btn btn-undo" onclick="setStatus('+i+',\\'pending\\')">↩ Undo Approve</button>' :
841
+ rule.status === 'rejected' ? '<button class="btn btn-undo" onclick="setStatus('+i+',\\'pending\\')">↩ Undo Reject</button>' :
842
+ '<button class="btn btn-approve" onclick="setStatus('+i+',\\'approved\\')">✔ Approve</button>' +
843
+ '<button class="btn btn-edit" onclick="toggleEdit('+i+')">✎ Edit</button>' +
844
+ '<button class="btn btn-reject" onclick="setStatus('+i+',\\'rejected\\')">✖ Reject</button>'}
845
+ </div>
846
+ <div class="edit-area" id="edit-\${i}">
847
+ <textarea id="edit-text-\${i}">\${esc(rule.plainEnglish)}</textarea>
848
+ <select id="edit-sev-\${i}">
849
+ <option value="critical" \${rule.severity==='critical'?'selected':''}>Critical</option>
850
+ <option value="high" \${rule.severity==='high'?'selected':''}>High</option>
851
+ <option value="medium" \${rule.severity==='medium'?'selected':''}>Medium</option>
852
+ <option value="low" \${rule.severity==='low'?'selected':''}>Low</option>
853
+ </select>
854
+ <div class="edit-actions">
855
+ <button class="btn btn-approve" onclick="saveEdit(\${i})">Save & Approve</button>
856
+ <button class="btn btn-undo" onclick="toggleEdit(\${i})">Cancel</button>
857
+ </div>
858
+ </div>
859
+ \`;
860
+ container.appendChild(card);
861
+ });
862
+ updateCounters();
863
+ }
864
+
865
+ function esc(s) { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
866
+
867
+ async function setStatus(i, status) {
868
+ rules[i].status = status;
869
+ await fetch('/api/rules/update', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id: rules[i].id, status }) });
870
+ renderCards();
871
+ }
872
+
873
+ function toggleEdit(i) {
874
+ const el = document.getElementById('edit-' + i);
875
+ el.classList.toggle('open');
876
+ }
877
+
878
+ async function saveEdit(i) {
879
+ const plainEnglish = document.getElementById('edit-text-' + i).value.trim();
880
+ const severity = document.getElementById('edit-sev-' + i).value;
881
+ rules[i].plainEnglish = plainEnglish;
882
+ rules[i].severity = severity;
883
+ rules[i].status = 'approved';
884
+ rules[i].editedByUser = true;
885
+ await fetch('/api/rules/update', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id: rules[i].id, status: 'approved', plainEnglish, severity }) });
886
+ renderCards();
887
+ }
888
+
889
+ async function finishReview() {
890
+ const res = await fetch('/api/finish', { method: 'POST' });
891
+ const data = await res.json();
892
+ if (data.ok) {
893
+ document.getElementById('done-msg').textContent = data.approved + ' rules approved. Run: korext policy publish --org <org-id>';
894
+ document.getElementById('done-overlay').classList.add('show');
895
+ } else {
896
+ alert(data.error || 'Failed to finish');
897
+ }
898
+ }
899
+
900
+ renderCards();
901
+ </script>
902
+ </body>
903
+ </html>`;
904
+ }
905
+
906
+ // ── korext policy publish ──────────────────────────────────────────────────
907
+ policyCmd
908
+ .command('publish')
909
+ .description('Publish approved rules to the Korext platform (sends rules only, never file content)')
910
+ .requiredOption('--org <orgId>', 'Your organisation ID')
911
+ .action(async (options) => {
912
+ if (!fs.existsSync(APPROVED_PACK_FILE)) {
913
+ console.error(chalk.red('No approved rules found.\nRun: korext policy review first.'));
914
+ process.exit(1);
915
+ }
916
+
917
+ const approved = JSON.parse(fs.readFileSync(APPROVED_PACK_FILE, 'utf-8'));
918
+ const token = getToken();
919
+
920
+ if (!token) {
921
+ console.error(chalk.red('✖ Authentication required for publishing.'));
922
+ console.error(chalk.dim(' Run: korext login <token>'));
923
+ process.exit(1);
924
+ }
925
+
926
+ const approvedRules = approved.rules.filter(r => r.status === 'approved');
927
+ if (approvedRules.length === 0) {
928
+ console.error(chalk.red('No approved rules found.\nRun: korext policy review first.'));
929
+ process.exit(1);
930
+ }
931
+
932
+ const spinner = ora(`Publishing ${approvedRules.length} rules to Korext (rules only — no document text)...`).start();
933
+
934
+ // CRITICAL DATA PROTECTION: Only send rule definitions
935
+ const publishPayload = {
936
+ packName: approved.packName,
937
+ packDescription: approved.packDescription || '',
938
+ orgId: options.org,
939
+ approvedRules: approvedRules.map(r => ({
940
+ ruleText: r.ruleText,
941
+ plainEnglish: r.plainEnglish,
942
+ severity: r.severity,
943
+ category: r.category,
944
+ codePatterns: r.codePatterns,
945
+ sourceSection: r.sourceSection || '',
946
+ regulatoryBasis: r.regulatoryBasis || null,
947
+ editedByUser: r.editedByUser || false
948
+ }))
949
+ // NOTE: No extractedText, no sourceFile path, no file contents
950
+ };
951
+
952
+ try {
953
+ const res = await fetch(`${API_URL}/api/packs/publish`, {
954
+ method: 'POST',
955
+ headers: {
956
+ 'Content-Type': 'application/json',
957
+ 'Authorization': `Bearer ${token}`
958
+ },
959
+ body: JSON.stringify(publishPayload)
960
+ });
961
+
962
+ if (!res.ok) {
963
+ const err = await res.json().catch(() => ({}));
964
+ spinner.fail(chalk.red(`Publish failed: ${err.error || `HTTP ${res.status}`}`));
965
+ process.exit(1);
966
+ }
967
+
968
+ const data = await res.json();
969
+ spinner.succeed(chalk.green('Policy pack published successfully.'));
970
+ console.log(` Pack ID: ${chalk.cyan(data.packId)}`);
971
+ console.log(` ${chalk.bold(approvedRules.length)} rules are now enforcing on your organisation.`);
972
+ console.log(` Developers will see violations on their next file save.\n`);
973
+
974
+ // Clean up local draft files
975
+ try {
976
+ const files = fs.readdirSync(LOCAL_DRAFT_DIR);
977
+ for (const f of files) {
978
+ if (f.startsWith('draft-') || f === 'approved-pack.json') {
979
+ fs.unlinkSync(path.join(LOCAL_DRAFT_DIR, f));
980
+ }
981
+ }
982
+ console.log(chalk.dim(' Local draft files cleaned up.'));
983
+ } catch (_) {}
984
+ } catch (e) {
985
+ spinner.fail(chalk.red(`Network error: ${e.message}`));
986
+ process.exit(1);
987
+ }
988
+ });
989
+
990
+ // ─── END POLICY COMMANDS ──────────────────────────────────────────────────
991
+
278
992
  program.parse(process.argv);
993
+
package/dummy.pdf ADDED
File without changes
package/korext.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "project": "my-app",
3
+ "targetPacks": [
4
+ "security-strict-v1",
5
+ "web-platform-v2"
6
+ ],
7
+ "exclude": [
8
+ "node_modules/**",
9
+ "dist/**"
10
+ ]
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "korext",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Korext Command Line Interface",
5
5
  "type": "module",
6
6
  "main": "bin/korext.js",
@@ -12,6 +12,7 @@
12
12
  "dependencies": {
13
13
  "chalk": "^4.1.2",
14
14
  "commander": "^14.0.3",
15
+ "form-data": "^4.0.5",
15
16
  "glob": "^13.0.6",
16
17
  "node-fetch": "^3.3.2",
17
18
  "ora": "^5.4.1"
@@ -22,5 +23,12 @@
22
23
  "glob",
23
24
  "node-fetch",
24
25
  "ora"
26
+ ],
27
+ "bundleDependencies": [
28
+ "chalk",
29
+ "commander",
30
+ "glob",
31
+ "node-fetch",
32
+ "ora"
25
33
  ]
26
34
  }
@@ -0,0 +1,146 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import fetch from 'node-fetch'; // Real-world implementations would bundle a client
5
+ import FormData from 'form-data';
6
+
7
+ const policyCmd = new Command('policy');
8
+
9
+ // CLI configuration assumptions based on enterprise integrations
10
+ const API_BASE = process.env.KOREXT_API_URL || 'http://localhost:3000/api';
11
+ const getToken = () => {
12
+ const tokenFile = path.join(process.env.HOME || '', '.korext', 'token');
13
+ if (fs.existsSync(tokenFile)) return fs.readFileSync(tokenFile, 'utf8').trim();
14
+ return null;
15
+ };
16
+
17
+ // =========================================================================
18
+ // init: Generate boilerplate for configuration
19
+ // =========================================================================
20
+ policyCmd
21
+ .command('init')
22
+ .description('Initialize a local korext policy configuration')
23
+ .action(() => {
24
+ const defaultCfg = {
25
+ project: "my-app",
26
+ targetPacks: ["security-strict-v1", "web-platform-v2"],
27
+ exclude: ["node_modules/**", "dist/**"]
28
+ };
29
+ fs.writeFileSync('korext.json', JSON.stringify(defaultCfg, null, 2));
30
+ console.log('✅ Created korext.json configuration');
31
+ });
32
+
33
+ // =========================================================================
34
+ // extract: Local extraction proxy to upload endoint
35
+ // =========================================================================
36
+ policyCmd
37
+ .command('extract <filePath>')
38
+ .description('Send a local PDF or MD to Korext to extract enforceable rules')
39
+ .option('-n, --name <name>', 'Name the custom policy pack')
40
+ .action(async (filePath, options) => {
41
+ const token = getToken();
42
+ if (!token) {
43
+ console.error('❌ Authentication required. Run `korext login` first.');
44
+ process.exit(1);
45
+ }
46
+
47
+ if (!fs.existsSync(filePath)) {
48
+ console.error(`❌ File not found: ${filePath}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const form = new FormData();
53
+ form.append('document', fs.createReadStream(filePath));
54
+ form.append('packName', options.name || path.basename(filePath));
55
+
56
+ try {
57
+ console.log(`📡 Uploading ${filePath} to Korext extractor...`);
58
+ const res = await fetch(`${API_BASE}/packs/upload`, {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Authorization': `Bearer ${token}`
62
+ },
63
+ body: form
64
+ });
65
+
66
+ const data = await res.json() as any;
67
+ if (!res.ok) throw new Error(data.error || 'Upload failed');
68
+
69
+ console.log('✅ Extraction complete.');
70
+ console.log(`📦 Policy Pack ID: ${data.pack.id}`);
71
+ console.log(`🔎 Found ${data.rules.length} extractable rules.`);
72
+ console.log('Run `korext policy review` to approve or reject these rules.');
73
+
74
+ // Cache the active pending pack ID for the next command locally
75
+ fs.writeFileSync('.korext-policy-pending', data.pack.id);
76
+ } catch (e: any) {
77
+ console.error(`❌ Extraction Error: ${e.message}`);
78
+ }
79
+ });
80
+
81
+ // =========================================================================
82
+ // review: Interactive CLI rule approval
83
+ // =========================================================================
84
+ policyCmd
85
+ .command('review [packId]')
86
+ .description('Review and approve rules pending in a custom policy pack')
87
+ .action(async (packId) => {
88
+ let targetPack = packId;
89
+ if (!targetPack) {
90
+ if (fs.existsSync('.korext-policy-pending')) {
91
+ targetPack = fs.readFileSync('.korext-policy-pending', 'utf8').trim();
92
+ } else {
93
+ console.error('❌ You must specify a Pack ID, or run `extract` first.');
94
+ process.exit(1);
95
+ }
96
+ }
97
+ console.log(`🌍 Opening local interface for review: https://app.korext.com/packs/${targetPack}/review`);
98
+ console.log(`✅ Please review and approve your extracted rules in the dashboard before publishing.`);
99
+ });
100
+
101
+ // =========================================================================
102
+ // publish: Activate a reviewed custom pack
103
+ // =========================================================================
104
+ policyCmd
105
+ .command('publish [packId]')
106
+ .description('Activate the policy pack, distributing it to your organization')
107
+ .option('-o, --org <orgId>', 'Organization ID to publish to')
108
+ .action(async (packId, options) => {
109
+ const token = getToken();
110
+ let targetPack = packId;
111
+ if (!targetPack) {
112
+ if (fs.existsSync('.korext-policy-pending')) {
113
+ targetPack = fs.readFileSync('.korext-policy-pending', 'utf8').trim();
114
+ } else {
115
+ console.error('❌ You must specify a Pack ID');
116
+ process.exit(1);
117
+ }
118
+ }
119
+
120
+ // Simulate sending only rules or metadata scoped to an org
121
+ const orgQuery = options.org ? `?orgId=${options.org}` : '';
122
+
123
+ try {
124
+ console.log(`🚀 Activating pack ${targetPack} ${options.org ? `for org ${options.org}` : ''}...`);
125
+ const res = await fetch(`${API_BASE}/packs/${targetPack}/activate${orgQuery}`, {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Authorization': `Bearer ${token}`
129
+ }
130
+ });
131
+
132
+ if (!res.ok) {
133
+ const d = await res.json() as any;
134
+ throw new Error(d.error || 'Failed activation');
135
+ }
136
+
137
+ console.log(`🎉 Pack ${targetPack} is now ACTIVE globally.`);
138
+ if (fs.existsSync('.korext-policy-pending')) {
139
+ fs.unlinkSync('.korext-policy-pending');
140
+ }
141
+ } catch (e: any) {
142
+ console.error(`❌ Activation Error: ${e.message}`);
143
+ }
144
+ });
145
+
146
+ export { policyCmd };
package/test-policy.md ADDED
@@ -0,0 +1,55 @@
1
+ # Internal Security Policy
2
+
3
+ ## Password Requirements
4
+
5
+ All applications must enforce the following password standards:
6
+
7
+ - Minimum password length of 12 characters
8
+ - Passwords must include uppercase, lowercase, numbers, and special characters
9
+ - Passwords must never be stored in plain text — use bcrypt or argon2 with a cost factor of at least 12
10
+ - Developers must not hardcode passwords, API keys, or secrets in source code
11
+
12
+ ## Data Encryption
13
+
14
+ All sensitive data must be encrypted at rest and in transit:
15
+
16
+ - Use AES-256 for encryption at rest
17
+ - TLS 1.2 or higher required for all network communication
18
+ - Do not use MD5 or SHA-1 for cryptographic purposes — they are considered broken
19
+ - All database connections must use encrypted transport
20
+
21
+ ## Input Validation
22
+
23
+ All user input must be validated before processing:
24
+
25
+ - Never concatenate user input directly into SQL queries — use parameterized queries
26
+ - Sanitize all HTML output to prevent cross-site scripting (XSS)
27
+ - Validate file upload types using magic bytes, not just file extension
28
+ - Limit request body size to prevent denial of service
29
+
30
+ ## Authentication and Session Management
31
+
32
+ Authentication systems must follow these rules:
33
+
34
+ - Implement rate limiting on login endpoints — maximum 10 attempts per minute
35
+ - Session tokens must be regenerated after successful authentication
36
+ - Set HttpOnly, Secure, and SameSite flags on all session cookies
37
+ - Session timeout must not exceed 30 minutes of inactivity
38
+
39
+ ## Dependency Management
40
+
41
+ All project dependencies must be managed securely:
42
+
43
+ - Do not use deprecated or abandoned libraries
44
+ - Run automated vulnerability scanning on all dependencies weekly
45
+ - Lock dependency versions in production builds
46
+ - Review all new dependencies for license compliance before adoption
47
+
48
+ ## Logging and Monitoring
49
+
50
+ Application logging must follow these standards:
51
+
52
+ - Never log sensitive data including passwords, tokens, PII, or credit card numbers
53
+ - All authentication events must be logged with timestamps and source IP
54
+ - Log retention period must be at least 90 days
55
+ - Implement alerting for repeated failed login attempts
package/test_cli.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { Command } from 'commander';
2
+ import { policyCmd } from './src/commands/policy.ts';
3
+ import fs from 'fs';
4
+
5
+ const program = new Command();
6
+ program.addCommand(policyCmd);
7
+
8
+ // Mock process.argv structure
9
+ const args = process.argv.slice(2);
10
+
11
+ // Run the interactive CLI inside the test environment
12
+ try {
13
+ program.parseAsync(['node', 'test_cli.ts', 'policy', ...args]);
14
+ } catch (e) {
15
+ console.error("CLI Execution failed", e);
16
+ }
Binary file