@fusengine/harness 0.1.78 → 0.1.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.mjs +628 -0
- package/package.json +1 -1
package/dist/cli/bin.mjs
CHANGED
|
@@ -293,6 +293,624 @@ function runSecurityScan(dir) {
|
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
//#endregion
|
|
296
|
+
//#region src/codex-rules/rules/rm-variants.ts
|
|
297
|
+
/**
|
|
298
|
+
* `rm` prefix rules — hand-mapped from `security.ts`'s two `rm -rf`-family
|
|
299
|
+
* CRITICAL_PATTERNS labels. Both source regexes accept `-rf`/`-fr` combined
|
|
300
|
+
* or `-r -f`/`-f -r` split flags, so each label needs 3 prefix_rule variants
|
|
301
|
+
* (combined flag + 2 split-flag orders) to avoid a flag-order gap.
|
|
302
|
+
*/
|
|
303
|
+
const RM_VARIANT_RULES = [
|
|
304
|
+
{
|
|
305
|
+
sourceLabel: "DANGEROUS PATTERN: recursive delete of / or ~",
|
|
306
|
+
pattern: [
|
|
307
|
+
"rm",
|
|
308
|
+
["-rf", "-fr"],
|
|
309
|
+
["/", "~"]
|
|
310
|
+
],
|
|
311
|
+
decision: "forbidden",
|
|
312
|
+
justification: "Recursive force-delete of the filesystem root or home directory.",
|
|
313
|
+
match: ["rm -rf /", "rm -fr ~"],
|
|
314
|
+
notMatch: ["rm -rf /tmp/build"]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
sourceLabel: "DANGEROUS PATTERN: recursive delete of / or ~",
|
|
318
|
+
pattern: [
|
|
319
|
+
"rm",
|
|
320
|
+
"-r",
|
|
321
|
+
"-f",
|
|
322
|
+
["/", "~"]
|
|
323
|
+
],
|
|
324
|
+
decision: "forbidden",
|
|
325
|
+
justification: "Recursive force-delete of the filesystem root or home directory (split flags).",
|
|
326
|
+
match: ["rm -r -f /", "rm -r -f ~"],
|
|
327
|
+
notMatch: ["rm -r -f /tmp/build"]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
sourceLabel: "DANGEROUS PATTERN: recursive delete of / or ~",
|
|
331
|
+
pattern: [
|
|
332
|
+
"rm",
|
|
333
|
+
"-f",
|
|
334
|
+
"-r",
|
|
335
|
+
["/", "~"]
|
|
336
|
+
],
|
|
337
|
+
decision: "forbidden",
|
|
338
|
+
justification: "Recursive force-delete of the filesystem root or home directory (split flags, reversed).",
|
|
339
|
+
match: ["rm -f -r /", "rm -f -r ~"],
|
|
340
|
+
notMatch: ["rm -f -r /tmp/build"]
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
sourceLabel: "DANGEROUS PATTERN: recursive delete of a system directory",
|
|
344
|
+
pattern: [
|
|
345
|
+
"rm",
|
|
346
|
+
["-rf", "-fr"],
|
|
347
|
+
[
|
|
348
|
+
"/etc",
|
|
349
|
+
"/usr",
|
|
350
|
+
"/var",
|
|
351
|
+
"/bin",
|
|
352
|
+
"/sbin",
|
|
353
|
+
"/boot",
|
|
354
|
+
"/lib"
|
|
355
|
+
]
|
|
356
|
+
],
|
|
357
|
+
decision: "forbidden",
|
|
358
|
+
justification: "Recursive force-delete of a core system directory.",
|
|
359
|
+
match: ["rm -rf /etc", "rm -fr /usr"],
|
|
360
|
+
notMatch: ["rm -rf /home/user/project"]
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
sourceLabel: "DANGEROUS PATTERN: recursive delete of a system directory",
|
|
364
|
+
pattern: [
|
|
365
|
+
"rm",
|
|
366
|
+
"-r",
|
|
367
|
+
"-f",
|
|
368
|
+
[
|
|
369
|
+
"/etc",
|
|
370
|
+
"/usr",
|
|
371
|
+
"/var",
|
|
372
|
+
"/bin",
|
|
373
|
+
"/sbin",
|
|
374
|
+
"/boot",
|
|
375
|
+
"/lib"
|
|
376
|
+
]
|
|
377
|
+
],
|
|
378
|
+
decision: "forbidden",
|
|
379
|
+
justification: "Recursive force-delete of a core system directory (split flags).",
|
|
380
|
+
match: ["rm -r -f /var", "rm -r -f /bin"],
|
|
381
|
+
notMatch: ["rm -r -f /home/user"]
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
sourceLabel: "DANGEROUS PATTERN: recursive delete of a system directory",
|
|
385
|
+
pattern: [
|
|
386
|
+
"rm",
|
|
387
|
+
"-f",
|
|
388
|
+
"-r",
|
|
389
|
+
[
|
|
390
|
+
"/etc",
|
|
391
|
+
"/usr",
|
|
392
|
+
"/var",
|
|
393
|
+
"/bin",
|
|
394
|
+
"/sbin",
|
|
395
|
+
"/boot",
|
|
396
|
+
"/lib"
|
|
397
|
+
]
|
|
398
|
+
],
|
|
399
|
+
decision: "forbidden",
|
|
400
|
+
justification: "Recursive force-delete of a core system directory (split flags, reversed).",
|
|
401
|
+
match: ["rm -f -r /sbin", "rm -f -r /lib"],
|
|
402
|
+
notMatch: ["rm -f -r /tmp"]
|
|
403
|
+
}
|
|
404
|
+
];
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/codex-rules/rules/mkfs.ts
|
|
407
|
+
/**
|
|
408
|
+
* `mkfs` filesystem-formatting variants — hand-mapped from `security.ts`'s
|
|
409
|
+
* `\bmkfs(?:\.[a-z0-9]+)?\b` CRITICAL_PATTERNS label. The source regex
|
|
410
|
+
* accepts ANY `.suffix`; `prefix_rule` cannot express an open-ended suffix,
|
|
411
|
+
* so every real-world `mkfs.*` filesystem type is enumerated explicitly as
|
|
412
|
+
* an alternation at position 0 (exhaustive, not a mechanical derivation).
|
|
413
|
+
*/
|
|
414
|
+
const MKFS_RULES = [{
|
|
415
|
+
sourceLabel: "CRITICAL: Detected dangerous command 'mkfs'",
|
|
416
|
+
pattern: [[
|
|
417
|
+
"mkfs",
|
|
418
|
+
"mkfs.ext2",
|
|
419
|
+
"mkfs.ext3",
|
|
420
|
+
"mkfs.ext4",
|
|
421
|
+
"mkfs.xfs",
|
|
422
|
+
"mkfs.btrfs",
|
|
423
|
+
"mkfs.exfat",
|
|
424
|
+
"mkfs.vfat",
|
|
425
|
+
"mkfs.fat",
|
|
426
|
+
"mkfs.ntfs",
|
|
427
|
+
"mkfs.f2fs",
|
|
428
|
+
"mkfs.hfsplus",
|
|
429
|
+
"mkfs.apfs",
|
|
430
|
+
"mkfs.msdos",
|
|
431
|
+
"mkfs.minix",
|
|
432
|
+
"mkfs.jfs",
|
|
433
|
+
"mkfs.reiserfs",
|
|
434
|
+
"mkfs.udf",
|
|
435
|
+
"mkfs.bfs",
|
|
436
|
+
"mkfs.cramfs"
|
|
437
|
+
]],
|
|
438
|
+
decision: "forbidden",
|
|
439
|
+
justification: "Formats a block device, destroying its contents (exhaustive mkfs.* variant enumeration).",
|
|
440
|
+
match: [
|
|
441
|
+
"mkfs.exfat /dev/sda1",
|
|
442
|
+
"mkfs.f2fs /dev/sdb1",
|
|
443
|
+
"mkfs /dev/sdb1",
|
|
444
|
+
"mkfs.ext4 /dev/sda1"
|
|
445
|
+
],
|
|
446
|
+
notMatch: ["ls /dev"]
|
|
447
|
+
}];
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/codex-rules/rules/diskutil.ts
|
|
450
|
+
/**
|
|
451
|
+
* `diskutil` erase/partition variants — hand-mapped from `security.ts`'s
|
|
452
|
+
* case-insensitive `\bdiskutil\s+(?:erase|partitionDisk)\b/i` CRITICAL
|
|
453
|
+
* label. `prefix_rule` has no case-insensitive matching, so every real
|
|
454
|
+
* casing macOS users actually type (camelCase, all-lowercase, all-caps) is
|
|
455
|
+
* enumerated explicitly as an alternation at position 1.
|
|
456
|
+
*/
|
|
457
|
+
const DISKUTIL_RULES = [{
|
|
458
|
+
sourceLabel: "CRITICAL: Detected dangerous command 'diskutil erase'",
|
|
459
|
+
pattern: ["diskutil", [
|
|
460
|
+
"erase",
|
|
461
|
+
"eraseDisk",
|
|
462
|
+
"erasedisk",
|
|
463
|
+
"ERASEDISK",
|
|
464
|
+
"eraseVolume",
|
|
465
|
+
"erasevolume",
|
|
466
|
+
"ERASEVOLUME",
|
|
467
|
+
"partitionDisk",
|
|
468
|
+
"partitiondisk",
|
|
469
|
+
"PARTITIONDISK"
|
|
470
|
+
]],
|
|
471
|
+
decision: "forbidden",
|
|
472
|
+
justification: "Erases or repartitions a macOS disk/volume (case-variant enumeration; execpolicy has no case-insensitive match).",
|
|
473
|
+
match: [
|
|
474
|
+
"diskutil eraseDisk /dev/disk2",
|
|
475
|
+
"diskutil ERASEDISK /dev/disk2",
|
|
476
|
+
"diskutil erase /dev/disk2"
|
|
477
|
+
],
|
|
478
|
+
notMatch: ["diskutil list"]
|
|
479
|
+
}];
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/codex-rules/rules/dd.ts
|
|
482
|
+
/**
|
|
483
|
+
* `dd` — hand-mapped from `security.ts`'s `CRITICAL: Detected dangerous
|
|
484
|
+
* command 'dd if='` label (source regex only fires on `of=/dev/...`).
|
|
485
|
+
* Owner decision: DOWNGRADE forbidden -> prompt. `prefix_rule` cannot
|
|
486
|
+
* glob-match a `key=value` argv token against a raw-device path, so a
|
|
487
|
+
* literal forbidden translation would silently block legitimate
|
|
488
|
+
* `dd if=a.img of=b.img` usage — prompting on every `dd` invocation instead
|
|
489
|
+
* never lets a raw-disk write through silently, without blocking safe use.
|
|
490
|
+
*/
|
|
491
|
+
const DD_RULES = [{
|
|
492
|
+
sourceLabel: "CRITICAL: Detected dangerous command 'dd if='",
|
|
493
|
+
pattern: ["dd"],
|
|
494
|
+
decision: "prompt",
|
|
495
|
+
justification: "dd can overwrite a raw block device via of=/dev/...; prompts on every dd invocation (broader than security.ts, which only fires on of=/dev/*) because execpolicy cannot glob-match that argv token — owner decision: do not block legitimate dd usage.",
|
|
496
|
+
match: ["dd if=/dev/zero of=/dev/sda", "dd if=a.img of=b.img"]
|
|
497
|
+
}];
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/codex-rules/rules/single-commands.ts
|
|
500
|
+
/**
|
|
501
|
+
* Single-purpose destructive binaries — hand-mapped 1:1 from `security.ts`'s
|
|
502
|
+
* `chmod 777 on /`, `shred`, `fdisk` CRITICAL_PATTERNS labels. `mkfs`,
|
|
503
|
+
* `dd`, and `diskutil erase` live in their own files (`mkfs.ts`, `dd.ts`,
|
|
504
|
+
* `diskutil.ts`) — each needed a wider case/variant enumeration than fits
|
|
505
|
+
* this file's SRP scope. The root-chmod rule needs the same flag-order
|
|
506
|
+
* alternation as `chmod-ask.ts` — source security.ts accepts flags before
|
|
507
|
+
* `777` for the root case too (`chmod -R 777 /` is still CRITICAL there).
|
|
508
|
+
*/
|
|
509
|
+
const SINGLE_COMMAND_RULES = [
|
|
510
|
+
{
|
|
511
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777 on /",
|
|
512
|
+
pattern: [
|
|
513
|
+
"chmod",
|
|
514
|
+
"777",
|
|
515
|
+
"/"
|
|
516
|
+
],
|
|
517
|
+
decision: "forbidden",
|
|
518
|
+
justification: "World-writable permissions on the filesystem root.",
|
|
519
|
+
match: ["chmod 777 /"]
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777 on /",
|
|
523
|
+
pattern: [
|
|
524
|
+
"chmod",
|
|
525
|
+
"-R",
|
|
526
|
+
"777",
|
|
527
|
+
"/"
|
|
528
|
+
],
|
|
529
|
+
decision: "forbidden",
|
|
530
|
+
justification: "World-writable permissions on the filesystem root, recursive.",
|
|
531
|
+
match: ["chmod -R 777 /"]
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777 on /",
|
|
535
|
+
pattern: [
|
|
536
|
+
"chmod",
|
|
537
|
+
"-r",
|
|
538
|
+
"777",
|
|
539
|
+
"/"
|
|
540
|
+
],
|
|
541
|
+
decision: "forbidden",
|
|
542
|
+
justification: "World-writable permissions on the filesystem root, recursive (lowercase flag).",
|
|
543
|
+
match: ["chmod -r 777 /"]
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777 on /",
|
|
547
|
+
pattern: [
|
|
548
|
+
"chmod",
|
|
549
|
+
"-v",
|
|
550
|
+
"777",
|
|
551
|
+
"/"
|
|
552
|
+
],
|
|
553
|
+
decision: "forbidden",
|
|
554
|
+
justification: "World-writable permissions on the filesystem root, verbose flag.",
|
|
555
|
+
match: ["chmod -v 777 /"]
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
sourceLabel: "CRITICAL: Detected dangerous command 'shred'",
|
|
559
|
+
pattern: ["shred"],
|
|
560
|
+
decision: "forbidden",
|
|
561
|
+
justification: "Irreversibly overwrites file contents.",
|
|
562
|
+
match: ["shred -u secrets.txt"],
|
|
563
|
+
notMatch: ["cat secrets.txt"]
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
sourceLabel: "CRITICAL: Detected dangerous command 'fdisk'",
|
|
567
|
+
pattern: ["fdisk"],
|
|
568
|
+
decision: "forbidden",
|
|
569
|
+
justification: "Edits disk partition tables.",
|
|
570
|
+
match: ["fdisk /dev/sda"],
|
|
571
|
+
notMatch: ["cat /proc/partitions"]
|
|
572
|
+
}
|
|
573
|
+
];
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/codex-rules/rules/privilege.ts
|
|
576
|
+
/**
|
|
577
|
+
* Privilege-escalation binaries — hand-mapped 1:1 from `security.ts`'s
|
|
578
|
+
* `sudo`/`su`/`doas`/`passwd`/`del` CRITICAL_PATTERNS labels (parity
|
|
579
|
+
* `security_rules.PRIVILEGE_COMMANDS`).
|
|
580
|
+
*/
|
|
581
|
+
const PRIVILEGE_RULES = [
|
|
582
|
+
{
|
|
583
|
+
sourceLabel: "PRIVILEGE ESCALATION: sudo",
|
|
584
|
+
pattern: ["sudo"],
|
|
585
|
+
decision: "forbidden",
|
|
586
|
+
justification: "Runs a command with elevated privileges.",
|
|
587
|
+
match: ["sudo rm -rf /tmp"],
|
|
588
|
+
notMatch: ["echo sudo"]
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
sourceLabel: "PRIVILEGE ESCALATION: su",
|
|
592
|
+
pattern: ["su"],
|
|
593
|
+
decision: "forbidden",
|
|
594
|
+
justification: "Switches to another user, typically root.",
|
|
595
|
+
match: ["su -", "su root"],
|
|
596
|
+
notMatch: ["sudo ls"]
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
sourceLabel: "PRIVILEGE ESCALATION: doas",
|
|
600
|
+
pattern: ["doas"],
|
|
601
|
+
decision: "forbidden",
|
|
602
|
+
justification: "BSD-style sudo alternative for elevated privileges.",
|
|
603
|
+
match: ["doas reboot"],
|
|
604
|
+
notMatch: ["ls"]
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
sourceLabel: "PRIVILEGE ESCALATION: passwd",
|
|
608
|
+
pattern: ["passwd"],
|
|
609
|
+
decision: "forbidden",
|
|
610
|
+
justification: "Changes a user's password.",
|
|
611
|
+
match: ["passwd root"],
|
|
612
|
+
notMatch: ["cat /etc/passwd"]
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
sourceLabel: "CRITICAL: Detected dangerous command 'del'",
|
|
616
|
+
pattern: ["del"],
|
|
617
|
+
decision: "forbidden",
|
|
618
|
+
justification: "Windows-style file deletion (parity security_rules.CRITICAL_COMMANDS).",
|
|
619
|
+
match: ["del file.txt"],
|
|
620
|
+
notMatch: ["delete.sh"]
|
|
621
|
+
}
|
|
622
|
+
];
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/codex-rules/rules/ask.ts
|
|
625
|
+
/**
|
|
626
|
+
* `prompt`-decision rules — hand-mapped 1:1 from `security.ts`'s
|
|
627
|
+
* ASK_PATTERNS. `DANGEROUS PATTERN: chmod 777` lives in `chmod-ask.ts`
|
|
628
|
+
* (needed multiple flag-order variants). `DANGEROUS PATTERN: write to /etc`
|
|
629
|
+
* is intentionally absent here: it fires on shell redirection
|
|
630
|
+
* (`> /etc/...`), which is not a single argv invocation — see
|
|
631
|
+
* `../skip-list.ts`.
|
|
632
|
+
*/
|
|
633
|
+
const ASK_RULES$1 = [
|
|
634
|
+
{
|
|
635
|
+
sourceLabel: "DANGEROUS PATTERN: recursive chown",
|
|
636
|
+
pattern: ["chown", "-R"],
|
|
637
|
+
decision: "prompt",
|
|
638
|
+
justification: "Recursive ownership change; confirm scope.",
|
|
639
|
+
match: ["chown -R user:user /srv/app"],
|
|
640
|
+
notMatch: ["chown user file.txt"]
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
sourceLabel: "DANGEROUS PATTERN: eval",
|
|
644
|
+
pattern: ["eval"],
|
|
645
|
+
decision: "prompt",
|
|
646
|
+
justification: "Evaluates arbitrary shell input.",
|
|
647
|
+
match: ["eval $(cat script.sh)"],
|
|
648
|
+
notMatch: ["node eval.js"]
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
sourceLabel: "DELETE: 'rm' permanently deletes - confirmation required",
|
|
652
|
+
pattern: ["rm"],
|
|
653
|
+
decision: "prompt",
|
|
654
|
+
justification: "rm permanently deletes; confirm before running.",
|
|
655
|
+
match: ["rm file.txt"],
|
|
656
|
+
notMatch: ["rmdir empty"]
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
sourceLabel: "DELETE: 'unlink' command detected - confirmation required",
|
|
660
|
+
pattern: ["unlink"],
|
|
661
|
+
decision: "prompt",
|
|
662
|
+
justification: "unlink permanently removes a file; confirm before running.",
|
|
663
|
+
match: ["unlink file.txt"],
|
|
664
|
+
notMatch: ["ln -s a b"]
|
|
665
|
+
}
|
|
666
|
+
];
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/codex-rules/rules/chmod-ask.ts
|
|
669
|
+
/**
|
|
670
|
+
* `chmod 777` `prompt`-decision variants — hand-mapped from `security.ts`'s
|
|
671
|
+
* `DANGEROUS PATTERN: chmod 777` ASK_PATTERNS label. The source regex
|
|
672
|
+
* accepts any chain of `-[a-zA-Z]+` flags before `777`; `prefix_rule` needs
|
|
673
|
+
* one literal-token variant per common flag order, so every frequent
|
|
674
|
+
* ordering is enumerated. Always `prompt`: `prefix_rule` cannot distinguish
|
|
675
|
+
* an absolute-path target from a relative one, so both are prompted rather
|
|
676
|
+
* than letting an absolute-path `chmod 777` fall through silently.
|
|
677
|
+
*/
|
|
678
|
+
const CHMOD_ASK_RULES = [
|
|
679
|
+
{
|
|
680
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777",
|
|
681
|
+
pattern: ["chmod", "777"],
|
|
682
|
+
decision: "prompt",
|
|
683
|
+
justification: "World-writable permissions; confirm the target is intended.",
|
|
684
|
+
match: ["chmod 777 file.txt", "chmod 777 /tmp/file"],
|
|
685
|
+
notMatch: ["chmod 755 file.txt"]
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777",
|
|
689
|
+
pattern: [
|
|
690
|
+
"chmod",
|
|
691
|
+
"-R",
|
|
692
|
+
"777"
|
|
693
|
+
],
|
|
694
|
+
decision: "prompt",
|
|
695
|
+
justification: "World-writable permissions, recursive; confirm the target is intended.",
|
|
696
|
+
match: ["chmod -R 777 dir"],
|
|
697
|
+
notMatch: ["chmod -R 755 dir"]
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777",
|
|
701
|
+
pattern: [
|
|
702
|
+
"chmod",
|
|
703
|
+
"-r",
|
|
704
|
+
"777"
|
|
705
|
+
],
|
|
706
|
+
decision: "prompt",
|
|
707
|
+
justification: "World-writable permissions, recursive (lowercase flag); confirm the target is intended.",
|
|
708
|
+
match: ["chmod -r 777 dir"]
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777",
|
|
712
|
+
pattern: [
|
|
713
|
+
"chmod",
|
|
714
|
+
"-v",
|
|
715
|
+
"777"
|
|
716
|
+
],
|
|
717
|
+
decision: "prompt",
|
|
718
|
+
justification: "World-writable permissions, verbose flag; confirm the target is intended.",
|
|
719
|
+
match: ["chmod -v 777 file.txt"]
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777",
|
|
723
|
+
pattern: [
|
|
724
|
+
"chmod",
|
|
725
|
+
"-Rv",
|
|
726
|
+
"777"
|
|
727
|
+
],
|
|
728
|
+
decision: "prompt",
|
|
729
|
+
justification: "World-writable permissions, combined recursive+verbose flags; confirm the target is intended.",
|
|
730
|
+
match: ["chmod -Rv 777 dir"]
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
sourceLabel: "DANGEROUS PATTERN: chmod 777",
|
|
734
|
+
pattern: [
|
|
735
|
+
"chmod",
|
|
736
|
+
"-vR",
|
|
737
|
+
"777"
|
|
738
|
+
],
|
|
739
|
+
decision: "prompt",
|
|
740
|
+
justification: "World-writable permissions, combined verbose+recursive flags; confirm the target is intended.",
|
|
741
|
+
match: ["chmod -vR 777 dir"]
|
|
742
|
+
}
|
|
743
|
+
];
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region src/codex-rules/rules/remote-fetch.ts
|
|
746
|
+
/**
|
|
747
|
+
* Remote-fetch tools (`curl`/`wget`) — targets the DOWNLOAD half of a
|
|
748
|
+
* `curl ... | sh` pipe instead of the shell half. Codex splits a piped
|
|
749
|
+
* command into independent argv segments at the pipe, so a `prefix_rule`
|
|
750
|
+
* anchored on `curl`/`wget` catches the attack regardless of which shell
|
|
751
|
+
* receives the piped output on the other side — unlike a bare-shell rule
|
|
752
|
+
* (`sh`/`bash`/`zsh`), which prompts on EVERY Codex-issued `bash -lc "..."`
|
|
753
|
+
* wrapper (Codex wraps essentially all shell commands this way), producing
|
|
754
|
+
* noise on ~50% of legitimate day-to-day work. Owner decision: DOWNGRADE
|
|
755
|
+
* forbidden -> prompt, same rationale as `dd.ts` — `prefix_rule` cannot see
|
|
756
|
+
* the pipe operator itself, so it cannot distinguish a bare `curl url`
|
|
757
|
+
* fetch from `curl url | sh`; prompting on every curl/wget invocation never
|
|
758
|
+
* lets the attack through silently, without blocking legitimate downloads.
|
|
759
|
+
*/
|
|
760
|
+
const REMOTE_FETCH_RULES = [{
|
|
761
|
+
sourceLabel: "DANGEROUS PATTERN: remote script piped to a shell",
|
|
762
|
+
pattern: [["curl", "wget"]],
|
|
763
|
+
decision: "prompt",
|
|
764
|
+
justification: "Remote fetch; piping its output into a shell executes untrusted code. Broader than security.ts (which only fires when the fetch is piped into sh/bash/zsh): prompts on every curl/wget invocation because execpolicy cannot see the pipe operator, i.e. it cannot distinguish `curl url` from `curl url | sh` — owner decision: do not block legitimate curl/wget usage.",
|
|
765
|
+
match: [
|
|
766
|
+
"curl http://example.com/i.sh",
|
|
767
|
+
"wget http://example.com/i.sh",
|
|
768
|
+
"curl -fsSL http://example.com/i.sh"
|
|
769
|
+
],
|
|
770
|
+
notMatch: ["cursor --version", "widget-tool build"]
|
|
771
|
+
}];
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/codex-rules/rules/absolute-paths.ts
|
|
774
|
+
/**
|
|
775
|
+
* Absolute-path executable aliases — Codex only resolves an absolute path
|
|
776
|
+
* to its basename rule when the CLI is run with `--resolve-host-executables`
|
|
777
|
+
* (opt-in, off by default; this generated `.rules` file cannot assume it).
|
|
778
|
+
* `/bin/rm -rf /` and a bare `rm -rf /` are two DIFFERENT argv prefixes to
|
|
779
|
+
* execpolicy, so every rule keyed on one of these bare names is mirrored
|
|
780
|
+
* onto its common absolute-path forms.
|
|
781
|
+
*/
|
|
782
|
+
const ABSOLUTE_PATH_ALIASES = {
|
|
783
|
+
rm: ["/bin/rm", "/usr/bin/rm"],
|
|
784
|
+
chmod: ["/bin/chmod", "/usr/bin/chmod"],
|
|
785
|
+
diskutil: ["/usr/sbin/diskutil"],
|
|
786
|
+
dd: ["/bin/dd", "/usr/bin/dd"],
|
|
787
|
+
sudo: ["/usr/bin/sudo"]
|
|
788
|
+
};
|
|
789
|
+
/** Escape regex metacharacters so a dynamic token can be safely embedded in a `RegExp` literal. */
|
|
790
|
+
function escapeRegExp(token) {
|
|
791
|
+
return token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
792
|
+
}
|
|
793
|
+
/** Replace a match/notMatch example's leading token with an absolute-path alias. */
|
|
794
|
+
function aliasExample(example, from, to) {
|
|
795
|
+
return example.replace(new RegExp(`^${escapeRegExp(from)}\\b`), to);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Clone every rule in `rules` whose first pattern token has a known
|
|
799
|
+
* absolute-path alias, producing one variant per alias (same decision,
|
|
800
|
+
* same sourceLabel — traced back to the same `security.ts` label).
|
|
801
|
+
*/
|
|
802
|
+
function withAbsolutePathAliases(rules) {
|
|
803
|
+
const out = [];
|
|
804
|
+
for (const rule of rules) {
|
|
805
|
+
const first = rule.pattern[0];
|
|
806
|
+
if (typeof first !== "string") continue;
|
|
807
|
+
const aliases = ABSOLUTE_PATH_ALIASES[first];
|
|
808
|
+
if (!aliases) continue;
|
|
809
|
+
for (const alias of aliases) out.push({
|
|
810
|
+
...rule,
|
|
811
|
+
pattern: [alias, ...rule.pattern.slice(1)],
|
|
812
|
+
match: rule.match.map((m) => aliasExample(m, first, alias)),
|
|
813
|
+
notMatch: rule.notMatch?.map((m) => aliasExample(m, first, alias)),
|
|
814
|
+
justification: `${rule.justification} (absolute-path form; Codex does not resolve executable basenames by default).`
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
return out;
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/codex-rules/skip-list.ts
|
|
821
|
+
/**
|
|
822
|
+
* `security.ts` labels with NO argv-prefix equivalent. Every entry here is
|
|
823
|
+
* shell syntax (pipes, redirection, function-definition syntax) that never
|
|
824
|
+
* appears as a plain argv token when a command is exec'd without a shell —
|
|
825
|
+
* Codex execpolicy's `prefix_rule` cannot express them. Covered instead by
|
|
826
|
+
* `sandbox_mode`/`approval_policy`. Never drop a label silently: it is
|
|
827
|
+
* either a prefix_rule (see `rules/`) or an entry here.
|
|
828
|
+
*/
|
|
829
|
+
const SKIP_LIST = [
|
|
830
|
+
{
|
|
831
|
+
sourceLabel: "DANGEROUS PATTERN: fork bomb",
|
|
832
|
+
reason: "Shell function-definition syntax `:(){ :|:& };:` is not a single argv invocation — no prefix_rule can express it."
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
sourceLabel: "DANGEROUS PATTERN: redirect to a raw disk device",
|
|
836
|
+
reason: "`>`/`>>` redirection to /dev/sda is shell syntax, invisible to argv-prefix matching."
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
sourceLabel: "DANGEROUS PATTERN: write to /etc",
|
|
840
|
+
reason: "`>`/`>>` redirection into /etc/* is shell syntax, invisible to argv-prefix matching."
|
|
841
|
+
}
|
|
842
|
+
];
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/codex-rules/mapping.ts
|
|
845
|
+
/** Every `decision = "forbidden"` rule, hand-mapped from CRITICAL_PATTERNS. */
|
|
846
|
+
const CRITICAL_BASE = [
|
|
847
|
+
...RM_VARIANT_RULES,
|
|
848
|
+
...MKFS_RULES,
|
|
849
|
+
...DISKUTIL_RULES,
|
|
850
|
+
...SINGLE_COMMAND_RULES,
|
|
851
|
+
...PRIVILEGE_RULES
|
|
852
|
+
];
|
|
853
|
+
/** Every `decision = "prompt"` rule, hand-mapped from ASK_PATTERNS (+ dd, downgraded). */
|
|
854
|
+
const ASK_BASE = [
|
|
855
|
+
...ASK_RULES$1,
|
|
856
|
+
...CHMOD_ASK_RULES,
|
|
857
|
+
...DD_RULES,
|
|
858
|
+
...REMOTE_FETCH_RULES
|
|
859
|
+
];
|
|
860
|
+
/** Absolute-path mirrors of the bare-name rules above (see `rules/absolute-paths.ts`). */
|
|
861
|
+
const ABSOLUTE_ALIASES = withAbsolutePathAliases([...CRITICAL_BASE, ...ASK_BASE]);
|
|
862
|
+
const CRITICAL_RULES = [...CRITICAL_BASE, ...ABSOLUTE_ALIASES.filter((r) => r.decision === "forbidden")];
|
|
863
|
+
const ASK_RULES = [...ASK_BASE, ...ABSOLUTE_ALIASES.filter((r) => r.decision === "prompt")];
|
|
864
|
+
/** Every generated `prefix_rule`, critical (`forbidden`) + ask (`prompt`). */
|
|
865
|
+
const ALL_RULES = [...CRITICAL_RULES, ...ASK_RULES];
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/codex-rules/starlark.ts
|
|
868
|
+
/**
|
|
869
|
+
* Neutralize characters that would break out of a Starlark `#` line comment
|
|
870
|
+
* (a bare `\n`/`\r` in an interpolated label/reason would start a new line
|
|
871
|
+
* no longer prefixed by `#`, corrupting the generated file).
|
|
872
|
+
*/
|
|
873
|
+
function sanitizeComment(text) {
|
|
874
|
+
return text.replace(/[\r\n]+/g, " ");
|
|
875
|
+
}
|
|
876
|
+
/** Render a string array as a Starlark list literal (double-quoted strings). */
|
|
877
|
+
function starlarkList(values) {
|
|
878
|
+
return `[${values.map((v) => JSON.stringify(v)).join(", ")}]`;
|
|
879
|
+
}
|
|
880
|
+
/** Render one pattern element: a bare token, or a `[...]` alternation. */
|
|
881
|
+
function starlarkPatternElement(el) {
|
|
882
|
+
return Array.isArray(el) ? starlarkList(el) : JSON.stringify(el);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Render a single `CodexPrefixRule` as a Starlark `prefix_rule(...)` call,
|
|
886
|
+
* prefixed by a traceability comment pointing back at its `security.ts` label.
|
|
887
|
+
*/
|
|
888
|
+
function renderPrefixRule(rule) {
|
|
889
|
+
const pattern = `[${rule.pattern.map(starlarkPatternElement).join(", ")}]`;
|
|
890
|
+
const lines = [
|
|
891
|
+
`# source: security.ts label "${sanitizeComment(rule.sourceLabel)}"`,
|
|
892
|
+
"prefix_rule(",
|
|
893
|
+
` pattern = ${pattern},`,
|
|
894
|
+
` decision = "${rule.decision}",`,
|
|
895
|
+
` justification = ${JSON.stringify(rule.justification)},`,
|
|
896
|
+
` match = ${starlarkList(rule.match)},`
|
|
897
|
+
];
|
|
898
|
+
if (rule.notMatch && rule.notMatch.length > 0) lines.push(` not_match = ${starlarkList(rule.notMatch)},`);
|
|
899
|
+
lines.push(")");
|
|
900
|
+
return lines.join("\n");
|
|
901
|
+
}
|
|
902
|
+
/** Render a list of rules, each block separated by a blank line. */
|
|
903
|
+
function renderPrefixRules(rules) {
|
|
904
|
+
return rules.map(renderPrefixRule).join("\n\n");
|
|
905
|
+
}
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/codex-rules/build.ts
|
|
908
|
+
const HEADER = "# Codex execpolicy rules — generated from @fusengine/harness src/policy/guards/security.ts\n# DO NOT hand-edit: regenerate via `harness codex-rules`.\n#\n# security.ts is the source of truth for dangerous-command detection; every\n# CRITICAL_PATTERNS label maps to decision = \"forbidden\", every ASK_PATTERNS\n# label maps to decision = \"prompt\". Strictest decision wins across matches.\n# The harness remains the AUTHORITATIVE enforcement layer — this file is a\n# complementary defense layer on the Codex side, not a replacement.\n#\n# IRREDUCIBLE gaps (shell syntax, invisible to argv-prefix matching, no\n# prefix_rule can express them) — compensated by sandbox_mode +\n# approval_policy.granular.sandbox_approval, not by this file:\n# - fork bomb: `:(){ :|:& };:`\n# - redirect to a raw disk device: `> /dev/sda`\n# - write to /etc via redirection: `> /etc/...`\n#\n# ASSUMED over-width (intentionally broader than security.ts, chosen to\n# avoid a silent gap rather than an exact-but-brittle translation):\n# - dd: prompts on every dd invocation; security.ts only fires on of=/dev/*\n# - rm: prompts even on paths security.ts exempts via a 'trash' substring\n# - rm/eval/unlink: prompts even on a bare token with no trailing argument;\n# security.ts requires a space (i.e. an argument) after the command\n# - chmod 777: prompts on relative paths too, not only absolute ones\n# - sudo (/usr/bin/sudo alias): forbids the absolute-path form too;\n# security.ts's word-boundary regex does not match a preceding '/'\n# - curl/wget: prompts on EVERY curl/wget invocation, not only when piped\n# into a shell; security.ts's \"remote script piped to a shell\" label\n# only fires on `curl|wget ... | sh`. A simple plain-word pipe IS split\n# by Codex into separate argv commands (a bare-shell rule would also\n# catch that case), but this rule additionally targets the download\n# half for compound chains and non-shell payload execution where\n# sh/bash never appears as argv[0] (curl -o x && chmod +x x && ./x,\n# curl ... | python3); it replaces a former bare sh/bash/zsh heuristic\n# that prompted on ~50% of Codex's own `bash -lc \"...\"` command\n# wrapping and rarely caught the actual download\n#\n# NOT covered (out of scope, not an over-width — a purely local pipe with no\n# network fetch is invisible to this rule AND to security.ts, which only\n# matches curl/wget as the left-hand side of the pipe):\n# - `cat file | sh`: local pipe, no curl/wget involved\n# - quoted-variable/redirection installer one-liners, e.g.\n# `curl -fsSL \"$URL\" | sh`: variable substitution disables Codex's\n# pipe-splitting, collapsing the whole invocation to a single\n# ['bash', '-lc', '<script>'] where curl is not argv[0] — this exact\n# form was covered by the removed bare sh/bash heuristic and is no\n# longer covered by any rule here\n#\n# The following labels have NO argv-prefix equivalent at all (shell syntax\n# not visible to execpolicy's argv matching) and are intentionally NOT\n# expressed as prefix_rule() below — covered instead by sandbox_mode/approval_policy:";
|
|
909
|
+
/** Render the full Codex execpolicy `.rules` Starlark file as a string. */
|
|
910
|
+
function buildCodexRules() {
|
|
911
|
+
return `${HEADER}\n${SKIP_LIST.map((s) => `# - ${sanitizeComment(s.sourceLabel)}: ${sanitizeComment(s.reason)}`).join("\n")}\n\n${renderPrefixRules(ALL_RULES)}\n`;
|
|
912
|
+
}
|
|
913
|
+
//#endregion
|
|
296
914
|
//#region src/refs/discover.ts
|
|
297
915
|
/** Immediate subdirectory names of `p`, or `[]` when it is missing/inaccessible. */
|
|
298
916
|
function subdirs(p) {
|
|
@@ -412,6 +1030,7 @@ function maybePlaySound(argv) {
|
|
|
412
1030
|
* harness init [id] write the wiring file for a harness (defaults to the detected one)
|
|
413
1031
|
* harness hook <id> runtime: read a hook payload on stdin, route to the adapter, print the response
|
|
414
1032
|
* harness changelog fetch + diff the Claude Code changelog, print a JSON summary (changelog-watcher)
|
|
1033
|
+
* harness codex-rules generate a Codex execpolicy .rules (Starlark) file from security.ts; stdout or --out <path>
|
|
415
1034
|
*/
|
|
416
1035
|
const hookDebug = process.env.FUSE_HARNESS_DEBUG === "1";
|
|
417
1036
|
function traceHook(label, data) {
|
|
@@ -503,6 +1122,15 @@ else if (cmd === "scan") {
|
|
|
503
1122
|
const dir = process.argv[3] ?? process.cwd();
|
|
504
1123
|
process.stdout.write(JSON.stringify(runSecurityScan(dir), null, 2) + "\n");
|
|
505
1124
|
process.exit(0);
|
|
1125
|
+
} else if (cmd === "codex-rules") {
|
|
1126
|
+
const outIdx = process.argv.indexOf("--out");
|
|
1127
|
+
const outPath = outIdx !== -1 ? process.argv[outIdx + 1] : void 0;
|
|
1128
|
+
const rules = buildCodexRules();
|
|
1129
|
+
if (outPath) {
|
|
1130
|
+
writeFileSync(outPath, rules);
|
|
1131
|
+
process.stderr.write(`harness: wrote Codex execpolicy rules -> ${outPath}\n`);
|
|
1132
|
+
} else process.stdout.write(rules);
|
|
1133
|
+
process.exit(0);
|
|
506
1134
|
} else {
|
|
507
1135
|
const files = stagedFiles();
|
|
508
1136
|
if (files.length === 0) process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.79",
|
|
4
4
|
"description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.ts",
|