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 +13 -0
- package/README.md +88 -0
- package/bin/korext.js +753 -38
- package/dummy.pdf +0 -0
- package/korext.json +11 -0
- package/package.json +9 -1
- package/src/commands/policy.ts +146 -0
- package/test-policy.md +55 -0
- package/test_cli.ts +16 -0
- package/korext-cli-0.1.0.tgz +0 -0
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
|
+
[](https://www.npmjs.com/package/korext)
|
|
7
|
+
[](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
|
-
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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 ');
|
|
247
|
-
else if (v.severity === 'high') { severityTag = chalk.red.bold('HIGH');
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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, '"').replace(/</g, '<');
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "korext",
|
|
3
|
-
"version": "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
|
+
}
|
package/korext-cli-0.1.0.tgz
DELETED
|
Binary file
|