hackmyagent 0.11.4 → 0.11.6

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.
@@ -104,6 +104,8 @@ const CHECK_PROJECT_TYPES = {
104
104
  'DNA-': ['all'],
105
105
  // Skill memory manipulation checks
106
106
  'SKILL-MEM-': ['openclaw', 'mcp'],
107
+ // NemoClaw/sandbox static analysis checks
108
+ 'NEMO-': ['all'],
107
109
  };
108
110
  // Patterns for detecting exposed credentials
109
111
  // Each pattern is carefully tuned to minimize false positives
@@ -383,6 +385,9 @@ class HardeningScanner {
383
385
  // Skill memory manipulation checks
384
386
  const skillMemFindings = await this.checkSkillMemory(targetDir, shouldFix);
385
387
  findings.push(...skillMemFindings);
388
+ // NemoClaw codebase pattern checks
389
+ const nemoFindings = await this.checkNemoClawPatterns(targetDir, shouldFix);
390
+ findings.push(...nemoFindings);
386
391
  // Enrich findings with attack taxonomy mapping
387
392
  (0, taxonomy_1.enrichWithTaxonomy)(findings);
388
393
  // Layer 2: Structural analysis (always on)
@@ -6558,6 +6563,599 @@ dist/
6558
6563
  }
6559
6564
  return findings;
6560
6565
  }
6566
+ /**
6567
+ * NemoClaw static analysis checks (NEMO-001 through NEMO-010)
6568
+ * Detects vulnerability patterns in any codebase: unsafe installs, missing
6569
+ * digest verification, injection vectors, secret leaks, deserialization, and
6570
+ * egress policy gaps.
6571
+ */
6572
+ async checkNemoClawPatterns(targetDir, _shouldFix) {
6573
+ const findings = [];
6574
+ // Collect source files by extension (max depth 5, skips node_modules/.git/dist/build)
6575
+ const shFiles = await this.walkDirectory(targetDir, ['.sh'], 0, 5);
6576
+ const tsJsFiles = await this.walkDirectory(targetDir, ['.ts', '.js'], 0, 5);
6577
+ const pyFiles = await this.walkDirectory(targetDir, ['.py'], 0, 5);
6578
+ const yamlFiles = await this.walkDirectory(targetDir, ['.yaml', '.yml'], 0, 5);
6579
+ // Cap file counts to avoid scanning enormous repos
6580
+ const maxFiles = 200;
6581
+ const cappedSh = shFiles.slice(0, maxFiles);
6582
+ const cappedTsJs = tsJsFiles.slice(0, maxFiles);
6583
+ const cappedPy = pyFiles.slice(0, maxFiles);
6584
+ const cappedYaml = yamlFiles.slice(0, maxFiles);
6585
+ // ---------- NEMO-001: Curl-pipe install without checksum ----------
6586
+ let nemo001Found = false;
6587
+ for (const file of cappedSh) {
6588
+ try {
6589
+ const content = await fs.readFile(file, 'utf-8');
6590
+ const lines = content.split('\n');
6591
+ for (let i = 0; i < lines.length; i++) {
6592
+ const line = lines[i];
6593
+ if (/curl.*\|\s*(ba)?sh/i.test(line) || /curl.*\|\s*sudo/i.test(line)) {
6594
+ // Check surrounding 20 lines for checksum verification
6595
+ const windowStart = Math.max(0, i - 10);
6596
+ const windowEnd = Math.min(lines.length, i + 11);
6597
+ const window = lines.slice(windowStart, windowEnd).join('\n').toLowerCase();
6598
+ if (!window.includes('sha256') && !window.includes('checksum') && !window.includes('gpg --verify')) {
6599
+ nemo001Found = true;
6600
+ findings.push({
6601
+ checkId: 'NEMO-001',
6602
+ name: 'Curl-pipe install without checksum',
6603
+ description: 'A shell script pipes curl output directly into a shell interpreter without verifying a checksum or GPG signature. An attacker who compromises the remote host can inject arbitrary code.',
6604
+ category: 'nemo-install',
6605
+ severity: 'critical',
6606
+ passed: false,
6607
+ message: `Curl-pipe install without integrity check at line ${i + 1}`,
6608
+ fixable: false,
6609
+ file: path.relative(targetDir, file),
6610
+ line: i + 1,
6611
+ fix: 'Download to temp file, verify checksum, then execute: curl -o /tmp/install.sh URL && echo "EXPECTED_SHA256 /tmp/install.sh" | sha256sum -c && bash /tmp/install.sh',
6612
+ });
6613
+ }
6614
+ }
6615
+ }
6616
+ }
6617
+ catch { /* skip unreadable */ }
6618
+ }
6619
+ if (!nemo001Found && cappedSh.length > 0) {
6620
+ findings.push({
6621
+ checkId: 'NEMO-001',
6622
+ name: 'Curl-pipe install without checksum',
6623
+ description: 'No curl-pipe-to-shell patterns found without checksum verification.',
6624
+ category: 'nemo-install',
6625
+ severity: 'critical',
6626
+ passed: true,
6627
+ message: 'No unsafe curl-pipe installs detected',
6628
+ fixable: false,
6629
+ });
6630
+ }
6631
+ // ---------- NEMO-002: Blueprint/artifact digest verification gap ----------
6632
+ let nemo002Found = false;
6633
+ // Check YAML files for empty digest fields
6634
+ for (const file of cappedYaml) {
6635
+ try {
6636
+ const content = await fs.readFile(file, 'utf-8');
6637
+ const lines = content.split('\n');
6638
+ for (let i = 0; i < lines.length; i++) {
6639
+ const line = lines[i];
6640
+ if (/digest:\s*["']?\s*["']?\s*$/.test(line) || /digest:\s*["']{2}/.test(line)) {
6641
+ nemo002Found = true;
6642
+ findings.push({
6643
+ checkId: 'NEMO-002',
6644
+ name: 'Empty digest field in blueprint/artifact',
6645
+ description: 'A YAML manifest declares a digest field with an empty value. Artifacts without digests cannot be verified for integrity, enabling supply-chain injection.',
6646
+ category: 'nemo-integrity',
6647
+ severity: 'critical',
6648
+ passed: false,
6649
+ message: `Empty digest field at line ${i + 1}`,
6650
+ fixable: false,
6651
+ file: path.relative(targetDir, file),
6652
+ line: i + 1,
6653
+ fix: 'Require non-empty digest; fail verification if digest is missing. Compute with: sha256sum <artifact>',
6654
+ });
6655
+ }
6656
+ }
6657
+ }
6658
+ catch { /* skip */ }
6659
+ }
6660
+ // Check TS/JS files for digest skip logic
6661
+ for (const file of cappedTsJs) {
6662
+ try {
6663
+ const content = await fs.readFile(file, 'utf-8');
6664
+ const lines = content.split('\n');
6665
+ for (let i = 0; i < lines.length; i++) {
6666
+ const line = lines[i];
6667
+ if (/if\s*\(\s*!?.*digest\s*&&/.test(line) || /if\s*\(\s*!.*digest\s*\)/.test(line)) {
6668
+ nemo002Found = true;
6669
+ findings.push({
6670
+ checkId: 'NEMO-002',
6671
+ name: 'Digest verification skipped on falsy value',
6672
+ description: 'Code skips digest verification when the digest field is falsy. An attacker who removes the digest from a manifest bypasses integrity checks entirely.',
6673
+ category: 'nemo-integrity',
6674
+ severity: 'critical',
6675
+ passed: false,
6676
+ message: `Digest verification skipped when falsy at line ${i + 1}`,
6677
+ fixable: false,
6678
+ file: path.relative(targetDir, file),
6679
+ line: i + 1,
6680
+ fix: 'Require non-empty digest; fail verification if digest is missing instead of skipping the check.',
6681
+ });
6682
+ }
6683
+ }
6684
+ }
6685
+ catch { /* skip */ }
6686
+ }
6687
+ if (!nemo002Found && (cappedYaml.length > 0 || cappedTsJs.length > 0)) {
6688
+ findings.push({
6689
+ checkId: 'NEMO-002',
6690
+ name: 'Blueprint/artifact digest verification gap',
6691
+ description: 'No empty digest fields or digest-skip logic found.',
6692
+ category: 'nemo-integrity',
6693
+ severity: 'critical',
6694
+ passed: true,
6695
+ message: 'No digest verification gaps detected',
6696
+ fixable: false,
6697
+ });
6698
+ }
6699
+ // ---------- NEMO-003: Hot-reload policy paths reachable from user input ----------
6700
+ let nemo003Found = false;
6701
+ for (const file of cappedTsJs) {
6702
+ try {
6703
+ const content = await fs.readFile(file, 'utf-8');
6704
+ const lines = content.split('\n');
6705
+ for (let i = 0; i < lines.length; i++) {
6706
+ const line = lines[i];
6707
+ if (/policy.*reload|reload.*policy|hot.*reload/i.test(line)) {
6708
+ // Check surrounding 10 lines for user input references
6709
+ const windowStart = Math.max(0, i - 5);
6710
+ const windowEnd = Math.min(lines.length, i + 6);
6711
+ const window = lines.slice(windowStart, windowEnd).join('\n');
6712
+ if (/req\.|request\.|input\.|user\./i.test(window)) {
6713
+ nemo003Found = true;
6714
+ findings.push({
6715
+ checkId: 'NEMO-003',
6716
+ name: 'Hot-reload policy path reachable from user input',
6717
+ description: 'A policy reload mechanism is within code proximity of user input handling. An attacker could trigger policy changes through crafted requests.',
6718
+ category: 'nemo-policy',
6719
+ severity: 'high',
6720
+ passed: false,
6721
+ message: `Policy reload near user input handling at line ${i + 1}`,
6722
+ fixable: false,
6723
+ file: path.relative(targetDir, file),
6724
+ line: i + 1,
6725
+ fix: 'Gate policy reload behind operator authentication, not agent output or user requests.',
6726
+ });
6727
+ }
6728
+ }
6729
+ }
6730
+ }
6731
+ catch { /* skip */ }
6732
+ }
6733
+ if (!nemo003Found && cappedTsJs.length > 0) {
6734
+ findings.push({
6735
+ checkId: 'NEMO-003',
6736
+ name: 'Hot-reload policy paths reachable from user input',
6737
+ description: 'No policy reload paths reachable from user input found.',
6738
+ category: 'nemo-policy',
6739
+ severity: 'high',
6740
+ passed: true,
6741
+ message: 'No unsafe policy reload paths detected',
6742
+ fixable: false,
6743
+ });
6744
+ }
6745
+ // ---------- NEMO-004: API key passed as CLI argument ----------
6746
+ let nemo004Found = false;
6747
+ const nemo004Files = [...cappedTsJs, ...cappedPy];
6748
+ for (const file of nemo004Files) {
6749
+ try {
6750
+ const content = await fs.readFile(file, 'utf-8');
6751
+ const lines = content.split('\n');
6752
+ for (let i = 0; i < lines.length; i++) {
6753
+ const line = lines[i];
6754
+ if (/--credential.*\$\{.*key/i.test(line) ||
6755
+ /--api-key.*\$\{/i.test(line) ||
6756
+ /--token.*\$\{/i.test(line) ||
6757
+ /execSync.*--credential/i.test(line) ||
6758
+ /spawn.*--credential/i.test(line) ||
6759
+ /subprocess.*--credential/i.test(line)) {
6760
+ nemo004Found = true;
6761
+ findings.push({
6762
+ checkId: 'NEMO-004',
6763
+ name: 'API key passed as CLI argument',
6764
+ description: 'Credentials are passed as command-line arguments to a subprocess. CLI arguments are visible in process listings (ps aux) and shell history.',
6765
+ category: 'nemo-secrets',
6766
+ severity: 'high',
6767
+ passed: false,
6768
+ message: `Credential passed as CLI argument at line ${i + 1}`,
6769
+ fixable: false,
6770
+ file: path.relative(targetDir, file),
6771
+ line: i + 1,
6772
+ fix: 'Pass credentials via environment variables or stdin, not command-line arguments.',
6773
+ });
6774
+ }
6775
+ }
6776
+ }
6777
+ catch { /* skip */ }
6778
+ }
6779
+ if (!nemo004Found && nemo004Files.length > 0) {
6780
+ findings.push({
6781
+ checkId: 'NEMO-004',
6782
+ name: 'API key passed as CLI argument',
6783
+ description: 'No credentials passed as CLI arguments detected.',
6784
+ category: 'nemo-secrets',
6785
+ severity: 'high',
6786
+ passed: true,
6787
+ message: 'No CLI credential exposure detected',
6788
+ fixable: false,
6789
+ });
6790
+ }
6791
+ // ---------- NEMO-005: exec() with user-controlled string interpolation ----------
6792
+ let nemo005Found = false;
6793
+ for (const file of cappedTsJs) {
6794
+ try {
6795
+ const content = await fs.readFile(file, 'utf-8');
6796
+ const lines = content.split('\n');
6797
+ for (let i = 0; i < lines.length; i++) {
6798
+ const line = lines[i];
6799
+ // Match exec( or execSync( with template literal containing user-controlled vars
6800
+ // Exclude execFile (safe) patterns
6801
+ if (/\bexec(Sync)?\s*\(/.test(line) &&
6802
+ !/\bexecFile/.test(line) &&
6803
+ /`[^`]*\$\{[^}]*(name|Name|id|Id|input|arg|param|flag|option)/i.test(line)) {
6804
+ nemo005Found = true;
6805
+ findings.push({
6806
+ checkId: 'NEMO-005',
6807
+ name: 'exec() with user-controlled string interpolation',
6808
+ description: 'exec() or execSync() is called with a template literal containing user-controlled variables. This enables command injection.',
6809
+ category: 'nemo-injection',
6810
+ severity: 'critical',
6811
+ passed: false,
6812
+ message: `exec() with user-controlled interpolation at line ${i + 1}`,
6813
+ fixable: false,
6814
+ file: path.relative(targetDir, file),
6815
+ line: i + 1,
6816
+ fix: 'Use execFile() or spawn() with array arguments instead of exec() with string interpolation.',
6817
+ });
6818
+ }
6819
+ }
6820
+ }
6821
+ catch { /* skip */ }
6822
+ }
6823
+ if (!nemo005Found && cappedTsJs.length > 0) {
6824
+ findings.push({
6825
+ checkId: 'NEMO-005',
6826
+ name: 'exec() with user-controlled string interpolation',
6827
+ description: 'No exec() calls with user-controlled string interpolation found.',
6828
+ category: 'nemo-injection',
6829
+ severity: 'critical',
6830
+ passed: true,
6831
+ message: 'No command injection via exec() detected',
6832
+ fixable: false,
6833
+ });
6834
+ }
6835
+ // ---------- NEMO-006: Predictable /tmp paths without mktemp ----------
6836
+ let nemo006Found = false;
6837
+ for (const file of cappedSh) {
6838
+ try {
6839
+ const content = await fs.readFile(file, 'utf-8');
6840
+ const lines = content.split('\n');
6841
+ for (let i = 0; i < lines.length; i++) {
6842
+ const line = lines[i];
6843
+ // Skip lines that ARE mktemp commands
6844
+ if (/mktemp/.test(line))
6845
+ continue;
6846
+ // Match hardcoded /tmp/ writes
6847
+ if (/\/tmp\//.test(line) &&
6848
+ (/>/.test(line) || />>/.test(line) || /-o\s+\/tmp\//.test(line) || /install.*\/tmp\//.test(line))) {
6849
+ nemo006Found = true;
6850
+ findings.push({
6851
+ checkId: 'NEMO-006',
6852
+ name: 'Predictable /tmp path without mktemp',
6853
+ description: 'A shell script writes to a hardcoded /tmp path instead of using mktemp. Predictable temp file names enable symlink attacks (CWE-377).',
6854
+ category: 'nemo-filesystem',
6855
+ severity: 'high',
6856
+ passed: false,
6857
+ message: `Hardcoded /tmp path at line ${i + 1}`,
6858
+ fixable: false,
6859
+ file: path.relative(targetDir, file),
6860
+ line: i + 1,
6861
+ fix: 'Use mktemp for all temporary files; add trap EXIT for cleanup: TMPDIR=$(mktemp -d) && trap "rm -rf $TMPDIR" EXIT',
6862
+ });
6863
+ }
6864
+ }
6865
+ }
6866
+ catch { /* skip */ }
6867
+ }
6868
+ if (!nemo006Found && cappedSh.length > 0) {
6869
+ findings.push({
6870
+ checkId: 'NEMO-006',
6871
+ name: 'Predictable /tmp paths without mktemp',
6872
+ description: 'No hardcoded /tmp paths found in shell scripts.',
6873
+ category: 'nemo-filesystem',
6874
+ severity: 'high',
6875
+ passed: true,
6876
+ message: 'No predictable temp file paths detected',
6877
+ fixable: false,
6878
+ });
6879
+ }
6880
+ // ---------- NEMO-007: Full process.env passthrough to subprocess ----------
6881
+ let nemo007Found = false;
6882
+ for (const file of cappedTsJs) {
6883
+ try {
6884
+ const content = await fs.readFile(file, 'utf-8');
6885
+ const lines = content.split('\n');
6886
+ for (let i = 0; i < lines.length; i++) {
6887
+ const line = lines[i];
6888
+ if (/env:\s*\{[^}]*\.\.\.process\.env/.test(line)) {
6889
+ nemo007Found = true;
6890
+ findings.push({
6891
+ checkId: 'NEMO-007',
6892
+ name: 'Full process.env passthrough to subprocess',
6893
+ description: 'process.env is spread into subprocess options, leaking all environment variables (including secrets) to child processes.',
6894
+ category: 'nemo-secrets',
6895
+ severity: 'high',
6896
+ passed: false,
6897
+ message: `Full process.env spread into subprocess at line ${i + 1}`,
6898
+ fixable: false,
6899
+ file: path.relative(targetDir, file),
6900
+ line: i + 1,
6901
+ fix: 'Use an explicit env var allowlist instead of spreading process.env: env: { PATH: process.env.PATH, NODE_ENV: process.env.NODE_ENV }',
6902
+ });
6903
+ }
6904
+ }
6905
+ }
6906
+ catch { /* skip */ }
6907
+ }
6908
+ if (!nemo007Found && cappedTsJs.length > 0) {
6909
+ findings.push({
6910
+ checkId: 'NEMO-007',
6911
+ name: 'Full process.env passthrough to subprocess',
6912
+ description: 'No full process.env passthrough to subprocesses found.',
6913
+ category: 'nemo-secrets',
6914
+ severity: 'high',
6915
+ passed: true,
6916
+ message: 'No process.env leakage to subprocesses detected',
6917
+ fixable: false,
6918
+ });
6919
+ }
6920
+ // ---------- NEMO-008: TOCTOU race between verify and apply ----------
6921
+ let nemo008Found = false;
6922
+ // Heuristic: look for verify/validate in one file AND exec/spawn in the same directory tree
6923
+ const dirVerifyMap = new Map();
6924
+ const dirExecMap = new Map();
6925
+ for (const file of cappedTsJs) {
6926
+ try {
6927
+ const content = await fs.readFile(file, 'utf-8');
6928
+ const lines = content.split('\n');
6929
+ const dir = path.dirname(file);
6930
+ for (let i = 0; i < lines.length; i++) {
6931
+ if (/verify.*digest|validate.*hash|check.*integrity/i.test(lines[i])) {
6932
+ if (!dirVerifyMap.has(dir)) {
6933
+ dirVerifyMap.set(dir, { verifyFile: file, verifyLine: i + 1 });
6934
+ }
6935
+ }
6936
+ if (/\bspawn\s*\(|\bexec\s*\(|\bexecSync\s*\(|\bexecFile\s*\(/.test(lines[i])) {
6937
+ if (!dirExecMap.has(dir)) {
6938
+ dirExecMap.set(dir, { execFile: file, execLine: i + 1 });
6939
+ }
6940
+ }
6941
+ }
6942
+ }
6943
+ catch { /* skip */ }
6944
+ }
6945
+ for (const [dir, verify] of dirVerifyMap) {
6946
+ const exec = dirExecMap.get(dir);
6947
+ if (exec && verify.verifyFile !== exec.execFile) {
6948
+ nemo008Found = true;
6949
+ findings.push({
6950
+ checkId: 'NEMO-008',
6951
+ name: 'TOCTOU race between verify and apply',
6952
+ description: 'Integrity verification and execution happen in separate files, creating a time-of-check-time-of-use window where the artifact can be swapped between verification and use.',
6953
+ category: 'nemo-integrity',
6954
+ severity: 'high',
6955
+ passed: false,
6956
+ message: `Verify in ${path.relative(targetDir, verify.verifyFile)}:${verify.verifyLine}, exec in ${path.relative(targetDir, exec.execFile)}:${exec.execLine}`,
6957
+ fixable: false,
6958
+ file: path.relative(targetDir, verify.verifyFile),
6959
+ line: verify.verifyLine,
6960
+ fix: 'Copy artifact to temp dir, verify the copy, execute from the copy (atomic verify-then-execute in the same function).',
6961
+ });
6962
+ }
6963
+ }
6964
+ if (!nemo008Found && cappedTsJs.length > 0) {
6965
+ findings.push({
6966
+ checkId: 'NEMO-008',
6967
+ name: 'TOCTOU race between verify and apply',
6968
+ description: 'No verify/apply TOCTOU patterns detected.',
6969
+ category: 'nemo-integrity',
6970
+ severity: 'high',
6971
+ passed: true,
6972
+ message: 'No TOCTOU race conditions detected',
6973
+ fixable: false,
6974
+ });
6975
+ }
6976
+ // ---------- NEMO-009: Unsafe deserialization of untrusted data ----------
6977
+ let nemo009Found = false;
6978
+ // Python files: pickle.load, yaml.load without SafeLoader, eval(), exec()
6979
+ for (const file of cappedPy) {
6980
+ try {
6981
+ const content = await fs.readFile(file, 'utf-8');
6982
+ const lines = content.split('\n');
6983
+ for (let i = 0; i < lines.length; i++) {
6984
+ const line = lines[i];
6985
+ if (/pickle\.load/i.test(line)) {
6986
+ nemo009Found = true;
6987
+ findings.push({
6988
+ checkId: 'NEMO-009',
6989
+ name: 'Unsafe deserialization: pickle.load',
6990
+ description: 'pickle.load() deserializes arbitrary Python objects, enabling remote code execution if the data source is untrusted.',
6991
+ category: 'nemo-deserialization',
6992
+ severity: 'critical',
6993
+ passed: false,
6994
+ message: `pickle.load() at line ${i + 1}`,
6995
+ fixable: false,
6996
+ file: path.relative(targetDir, file),
6997
+ line: i + 1,
6998
+ fix: 'Use json.load() or a restricted deserializer instead of pickle for untrusted data.',
6999
+ });
7000
+ }
7001
+ if (/yaml\.load\s*\(/.test(line) && !/Loader\s*=\s*SafeLoader/.test(line) && !/safe_load/.test(line)) {
7002
+ nemo009Found = true;
7003
+ findings.push({
7004
+ checkId: 'NEMO-009',
7005
+ name: 'Unsafe deserialization: yaml.load without SafeLoader',
7006
+ description: 'yaml.load() without SafeLoader can execute arbitrary Python code embedded in YAML documents.',
7007
+ category: 'nemo-deserialization',
7008
+ severity: 'high',
7009
+ passed: false,
7010
+ message: `yaml.load() without SafeLoader at line ${i + 1}`,
7011
+ fixable: false,
7012
+ file: path.relative(targetDir, file),
7013
+ line: i + 1,
7014
+ fix: 'Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader).',
7015
+ });
7016
+ }
7017
+ if (/\beval\s*\(/.test(line) || /\bexec\s*\(/.test(line)) {
7018
+ nemo009Found = true;
7019
+ findings.push({
7020
+ checkId: 'NEMO-009',
7021
+ name: 'Unsafe deserialization: eval/exec in Python',
7022
+ description: 'eval() or exec() executes arbitrary code. If the input originates from untrusted sources, this enables code injection.',
7023
+ category: 'nemo-deserialization',
7024
+ severity: 'critical',
7025
+ passed: false,
7026
+ message: `eval()/exec() at line ${i + 1}`,
7027
+ fixable: false,
7028
+ file: path.relative(targetDir, file),
7029
+ line: i + 1,
7030
+ fix: 'Replace eval/exec with ast.literal_eval() for data parsing, or use a safe DSL.',
7031
+ });
7032
+ }
7033
+ }
7034
+ }
7035
+ catch { /* skip */ }
7036
+ }
7037
+ // TS/JS files: eval(), new Function(), JSON5.parse
7038
+ for (const file of cappedTsJs) {
7039
+ try {
7040
+ const content = await fs.readFile(file, 'utf-8');
7041
+ const lines = content.split('\n');
7042
+ for (let i = 0; i < lines.length; i++) {
7043
+ const line = lines[i];
7044
+ if (/\beval\s*\(/.test(line)) {
7045
+ nemo009Found = true;
7046
+ findings.push({
7047
+ checkId: 'NEMO-009',
7048
+ name: 'Unsafe deserialization: eval()',
7049
+ description: 'eval() executes arbitrary JavaScript code. If the input comes from untrusted sources, this enables code injection.',
7050
+ category: 'nemo-deserialization',
7051
+ severity: 'critical',
7052
+ passed: false,
7053
+ message: `eval() at line ${i + 1}`,
7054
+ fixable: false,
7055
+ file: path.relative(targetDir, file),
7056
+ line: i + 1,
7057
+ fix: 'Use JSON.parse() for data, or a sandboxed evaluator for expressions.',
7058
+ });
7059
+ }
7060
+ if (/new\s+Function\s*\(/.test(line)) {
7061
+ nemo009Found = true;
7062
+ findings.push({
7063
+ checkId: 'NEMO-009',
7064
+ name: 'Unsafe deserialization: new Function()',
7065
+ description: 'new Function() creates executable code from strings, equivalent to eval() for code injection risks.',
7066
+ category: 'nemo-deserialization',
7067
+ severity: 'critical',
7068
+ passed: false,
7069
+ message: `new Function() at line ${i + 1}`,
7070
+ fixable: false,
7071
+ file: path.relative(targetDir, file),
7072
+ line: i + 1,
7073
+ fix: 'Use JSON.parse() for data, or a sandboxed evaluator for expressions.',
7074
+ });
7075
+ }
7076
+ if (/JSON5\.parse/.test(line)) {
7077
+ nemo009Found = true;
7078
+ findings.push({
7079
+ checkId: 'NEMO-009',
7080
+ name: 'Unsafe deserialization: JSON5.parse',
7081
+ description: 'JSON5.parse() is more lenient than JSON.parse(), accepting comments, trailing commas, and unquoted keys. This expanded surface can introduce parsing ambiguities.',
7082
+ category: 'nemo-deserialization',
7083
+ severity: 'high',
7084
+ passed: false,
7085
+ message: `JSON5.parse() at line ${i + 1}`,
7086
+ fixable: false,
7087
+ file: path.relative(targetDir, file),
7088
+ line: i + 1,
7089
+ fix: 'Use JSON.parse() instead of JSON5.parse() for untrusted data.',
7090
+ });
7091
+ }
7092
+ }
7093
+ }
7094
+ catch { /* skip */ }
7095
+ }
7096
+ if (!nemo009Found && (cappedPy.length > 0 || cappedTsJs.length > 0)) {
7097
+ findings.push({
7098
+ checkId: 'NEMO-009',
7099
+ name: 'Unsafe deserialization of untrusted data',
7100
+ description: 'No unsafe deserialization patterns detected.',
7101
+ category: 'nemo-deserialization',
7102
+ severity: 'critical',
7103
+ passed: true,
7104
+ message: 'No unsafe deserialization detected',
7105
+ fixable: false,
7106
+ });
7107
+ }
7108
+ // ---------- NEMO-010: Network egress policy allows data exfiltration ----------
7109
+ let nemo010Found = false;
7110
+ const egressEndpoints = [
7111
+ 'api.telegram.org',
7112
+ 'discord.com/api',
7113
+ 'hooks.slack.com',
7114
+ 'webhook.site',
7115
+ 'requestbin',
7116
+ ];
7117
+ for (const file of cappedYaml) {
7118
+ try {
7119
+ const content = await fs.readFile(file, 'utf-8');
7120
+ const lines = content.split('\n');
7121
+ for (let i = 0; i < lines.length; i++) {
7122
+ const line = lines[i].toLowerCase();
7123
+ for (const endpoint of egressEndpoints) {
7124
+ if (line.includes(endpoint.toLowerCase())) {
7125
+ nemo010Found = true;
7126
+ findings.push({
7127
+ checkId: 'NEMO-010',
7128
+ name: 'Messaging API in egress policy',
7129
+ description: `Sandbox policy pre-allows access to ${endpoint}. Agents can exfiltrate data via messaging APIs without explicit operator approval.`,
7130
+ category: 'nemo-egress',
7131
+ severity: 'high',
7132
+ passed: false,
7133
+ message: `Messaging endpoint "${endpoint}" in egress policy at line ${i + 1}`,
7134
+ fixable: false,
7135
+ file: path.relative(targetDir, file),
7136
+ line: i + 1,
7137
+ fix: 'Remove messaging APIs from base sandbox policy; require explicit operator opt-in per deployment.',
7138
+ });
7139
+ }
7140
+ }
7141
+ }
7142
+ }
7143
+ catch { /* skip */ }
7144
+ }
7145
+ if (!nemo010Found && cappedYaml.length > 0) {
7146
+ findings.push({
7147
+ checkId: 'NEMO-010',
7148
+ name: 'Network egress policy allows data exfiltration',
7149
+ description: 'No messaging API endpoints found in egress policies.',
7150
+ category: 'nemo-egress',
7151
+ severity: 'high',
7152
+ passed: true,
7153
+ message: 'No exfiltration-prone egress endpoints detected',
7154
+ fixable: false,
7155
+ });
7156
+ }
7157
+ return findings;
7158
+ }
6561
7159
  }
6562
7160
  exports.HardeningScanner = HardeningScanner;
6563
7161
  // Files that may be created or modified during auto-fix