code wiki / _hdl_build / nx_secret_scan.nx

nx_secret_scan.nx source

↩ module page · 250 lines · 11256 B

1// nx_secret_scan.nx -- the team's PLAINTEXT-CREDENTIAL scanner (Sentinel lane; the DETECT half 2// of secret self-management -- nx_secret_cli is the STORE half, nx_security_sentinel ESCALATES). 3// 4// What it does, every run: 5// 1. Walks a DATA-DRIVEN root table (repo root depth-1, bench/ops, runtime/_hdl_build, _offc). 6// 2. For each regular TEXT file (binary = NUL byte in first 512B -> skipped; >4MiB capped), 7// counts occurrences of a DATA-DRIVEN credential-pattern table. The quote-bearing patterns 8// are ASSEMBLED AT RUNTIME (byte append) so this source never matches itself; files whose 9// NAME contains "secret_scan" are skipped for the same reason. 10// 3. Each finding -> one line to stdout + one ISSUE line with marker PLAINTEXT-SECRET appended 11// to knowledge/status/issues_durable.log (the signal stream nx_security_sentinel watches -- 12// its PLAINTEXT-SECRET row escalates to PM; resolve via SENTINEL-RESOLVE after migration). 13// 4. Appends one SECRET-SCAN verdict line to knowledge/status/secret_scan.log. 14// Exit 0 = clean, 1 = findings (re-runnable gate: clean tree MUST exit 0). 15// 16// Adding coverage = adding a pattern row or a root row (build intelligence, never strip). 17// The FIX for a finding is never "delete the script" -- it is migration to the vault path: 18// _offc/nx_secret_cli.elf get <name> -> /tmp/nxsecret.out (see _nx_vault_ssh.sh for the shape). 19// license_tier: ORIGINAL 20import "nx_syscalls.nx" 21const SCAN_MAGIC_4096: i64 = 4096 22 23const SCAN_FILE_MAX: i64 = 4194304 // 4 MiB read cap per file 24const SCAN_DIRBUF: i64 = 1048576 25const SCAN_ROOTS: i64 = 4 26const SCAN_PATS: i64 = 6 27 28func _p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 29func _fp(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 } 30func _fn(fd: i64, v: i64) -> i64 { let bb: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m}; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48;k=1}; while m>0{t[k]=48+(m%10);m=m/10;k=k+1}; var i: i64=0; while i<k{bb[i]=t[k-1-i];i=i+1}; sys_write(fd,bb,k); return 0 } 31func ss_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 32func ss_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i } 33 34// read whole file (cap); return bytes, -1 if open failed 35func ss_read(path: *u8, buf: *u8, cap: i64) -> i64 { 36 let fd: i64 = sys_openat_rd(path) 37 if fd < 0 { return 0 - 1 } 38 var n: i64 = 0 39 var go: i64 = 1 40 while go == 1 { 41 let base: i64 = buf as i64 42 let r: i64 = sys_read(fd, (base + n) as *u8, cap - n) 43 if r <= 0 { go = 0 } else { n = n + r } 44 if n >= cap { go = 0 } 45 } 46 sys_close(fd) 47 return n 48} 49 50// non-overlapping occurrence count of pat in buf[0..n). 51// qch != 0 marks a quote-bearing credential pattern: a match immediately followed by the 52// SAME quote char is an EMPTY literal (PW="" = the shred-after-use idiom, hygiene not a 53// leak) and does not count. Intelligence, not suppression: nonempty values always count. 54func ss_count(buf: *u8, n: i64, pat: *u8, qch: i64) -> i64 { 55 let pl: i64 = ss_slen(pat) 56 if pl == 0 { return 0 } 57 var cnt: i64 = 0 58 var i: i64 = 0 59 while i + pl <= n { 60 var k: i64 = 0 61 var hit: i64 = 1 62 while k < pl { 63 if buf[i+k] != pat[k] { hit = 0; k = pl } else { k = k + 1 } 64 } 65 if hit == 1 { 66 if qch != 0 { 67 if i + pl < n { if (buf[i+pl] as i64) == qch { hit = 0 } } 68 } 69 if hit == 1 { cnt = cnt + 1 } 70 i = i + pl 71 } else { i = i + 1 } 72 } 73 return cnt 74} 75 76// does name contain sub? 77func ss_has_sub(name: *u8, sub: *u8) -> i64 { 78 let nl: i64 = ss_slen(name) 79 return ss_count(name, nl, sub, 0) 80} 81 82// binary sniff: NUL byte in first 512 bytes 83func ss_is_binary(buf: *u8, n: i64) -> i64 { 84 var lim: i64 = n 85 if lim > 512 { lim = 512 } 86 var i: i64 = 0 87 while i < lim { 88 if buf[i] == (0 as u8) { return 1 } 89 i = i + 1 90 } 91 return 0 92} 93 94// scan one file against the pattern tables (tabs[0]=pats tabs[1]=keys tabs[2]=qchs); 95// emit findings; return finding count 96func ss_scan_file(path: *u8, fbuf: *u8, tabs: *i64, ifd: i64) -> i64 { 97 let pats: *i64 = tabs[0] as *i64 98 let keys: *i64 = tabs[1] as *i64 99 let qchs: *i64 = tabs[2] as *i64 100 let n: i64 = ss_read(path, fbuf, SCAN_FILE_MAX) 101 if n <= 0 { return 0 } 102 if ss_is_binary(fbuf, n) == 1 { return 0 } 103 var found: i64 = 0 104 var pi: i64 = 0 105 while pi < SCAN_PATS { 106 let cnt: i64 = ss_count(fbuf, n, pats[pi] as *u8, qchs[pi]) 107 if cnt > 0 { 108 _p("SECRET-SCAN finding file=" as *u8); _p(path) 109 _p(" pat=" as *u8); _p(keys[pi] as *u8) 110 _p(" count=" as *u8); _fn(1, cnt); _p("\n" as *u8) 111 _fp(ifd, "ISSUE PLAINTEXT-SECRET file=" as *u8); _fp(ifd, path) 112 _fp(ifd, " pat=" as *u8); _fp(ifd, keys[pi] as *u8) 113 _fp(ifd, " count=" as *u8); _fn(ifd, cnt) 114 _fp(ifd, " :: credential literal committed in-repo; migrate to the vault path (_offc/nx_secret_cli.elf get <name>), never scrub-and-keep-plaintext\n" as *u8) 115 found = found + cnt 116 } 117 pi = pi + 1 118 } 119 return found 120} 121 122// walk one root dir (depth 1); returns findings; counts files via files_out[0] 123func ss_walk_root(root: *u8, fbuf: *u8, tabs: *i64, ifd: i64, files_out: *i64) -> i64 { 124 let dfd: i64 = sys_openat_rd(root) 125 if dfd < 0 { return 0 } 126 let dirbuf: *u8 = sys_mmap(SCAN_DIRBUF + 64) 127 let pathbuf: *u8 = sys_mmap(SCAN_MAGIC_4096) 128 var findings: i64 = 0 129 var done: i64 = 0 130 while done == 0 { 131 let nb: i64 = sys_getdents64(dfd, dirbuf, SCAN_DIRBUF) 132 if nb <= 0 { done = 1 } 133 else { 134 var off: i64 = 0 135 while off < nb { 136 let base: i64 = dirbuf as i64 137 let rec: *u8 = (base + off) as *u8 138 let rl: i64 = dirent_reclen(rec) 139 if rl <= 0 { off = nb } 140 else { 141 let name: *u8 = dirent_name(rec) 142 var want: i64 = 0 143 if dirent_type(rec) == 8 { want = 1 } 144 if ss_has_sub(name, "secret_scan" as *u8) > 0 { want = 0 } 145 if want == 1 { 146 var p: i64 = 0 147 // root "." -> bare name so paths read repo-relative 148 if root[0] == (46 as u8) { 149 if root[1] == (0 as u8) { p = 0 - 1 } 150 } 151 if p == 0 { 152 p = ss_cat(pathbuf, 0, root) 153 pathbuf[p] = 47 as u8 154 p = p + 1 155 } else { p = 0 } 156 p = ss_cat(pathbuf, p, name) 157 pathbuf[p] = 0 as u8 158 findings = findings + ss_scan_file(pathbuf, fbuf, tabs, ifd) 159 files_out[0] = files_out[0] + 1 160 } 161 off = off + rl 162 } 163 } 164 } 165 } 166 sys_close(dfd) 167 return findings 168} 169 170func main(argc: i64, argv: *i64) -> i64 { 171 _p("=== SECRET SCAN: detect plaintext credentials in-repo (Sentinel lane, verb: WATCH) ===\n" as *u8) 172 // argv[1] (optional) = alternate issues-log path -- the GATE plants fixtures and must 173 // not pollute the production signal stream the sentinel watches 174 var ipath: *u8 = "knowledge/status/issues_durable.log" as *u8 175 if argc >= 2 { ipath = argv[1] as *u8 } 176 177 // ---- PATTERN TABLE (quote-bearing rows assembled at runtime; data-driven) ---- 178 let pats: *i64 = sys_mmap(8 * SCAN_PATS) as *i64 179 let keys: *i64 = sys_mmap(8 * SCAN_PATS) as *i64 180 let pq: *u8 = sys_mmap(256) 181 var o: i64 = 0 182 pats[0] = (pq as i64) + o 183 o = ss_cat(pq, o, "PW=" as *u8); pq[o] = 34 as u8; o = o + 1; pq[o] = 0 as u8; o = o + 1 184 pats[1] = (pq as i64) + o 185 o = ss_cat(pq, o, "PW=" as *u8); pq[o] = 39 as u8; o = o + 1; pq[o] = 0 as u8; o = o + 1 186 pats[2] = (pq as i64) + o 187 o = ss_cat(pq, o, "PASSWORD=" as *u8); pq[o] = 34 as u8; o = o + 1; pq[o] = 0 as u8; o = o + 1 188 pats[3] = (pq as i64) + o 189 o = ss_cat(pq, o, "PASSWORD=" as *u8); pq[o] = 39 as u8; o = o + 1; pq[o] = 0 as u8; o = o + 1 190 pats[4] = (pq as i64) + o 191 o = ss_cat(pq, o, "sshpass -p" as *u8); pq[o] = 0 as u8; o = o + 1 192 pats[5] = (pq as i64) + o 193 o = ss_cat(pq, o, "PRIVATE KEY-----" as *u8); pq[o] = 0 as u8; o = o + 1 194 keys[0] = "PW-DQUOTE" as *u8 as i64 195 keys[1] = "PW-SQUOTE" as *u8 as i64 196 keys[2] = "PASSWORD-DQUOTE" as *u8 as i64 197 keys[3] = "PASSWORD-SQUOTE" as *u8 as i64 198 keys[4] = "SSHPASS" as *u8 as i64 199 keys[5] = "PRIVATE-KEY" as *u8 as i64 200 // quote char per row (0 = not quote-bearing): an immediately-following same quote 201 // means an EMPTY literal (the PW="" shred idiom) -> not a credential 202 let qchs: *i64 = sys_mmap(8 * SCAN_PATS) as *i64 203 qchs[0] = 34; qchs[1] = 39; qchs[2] = 34; qchs[3] = 39; qchs[4] = 0; qchs[5] = 0 204 let tabs: *i64 = sys_mmap(8 * 3) as *i64 205 tabs[0] = pats as i64 206 tabs[1] = keys as i64 207 tabs[2] = qchs as i64 208 209 // ---- ROOT TABLE (depth-1 each; add a row = add coverage) ---- 210 let roots: *i64 = sys_mmap(8 * SCAN_ROOTS) as *i64 211 roots[0] = "." as *u8 as i64 212 roots[1] = "bench/ops" as *u8 as i64 213 roots[2] = "runtime/_hdl_build" as *u8 as i64 214 roots[3] = "_offc" as *u8 as i64 215 216 let ifd: i64 = sys_openat_append(ipath, 0x1a4) 217 if ifd < 0 { _p(" issues log open failed\n" as *u8); sys_exit(2); return 2 } 218 let fbuf: *u8 = sys_mmap(SCAN_FILE_MAX + 64) 219 let files: *i64 = sys_mmap(16) as *i64 220 files[0] = 0 221 222 var findings: i64 = 0 223 var ri: i64 = 0 224 // GATE/KAT mode (argc>=2: argv[1]=alt issues log) only verifies the DETECTOR on the 225 // fixture, which is planted at root "." -> scan just root[0], skip the full-tree walk. 226 // The PRODUCTION posture scan (no argv) keeps full SCAN_ROOTS coverage. The detector 227 // logic is identical; only the KAT scope shrinks. ~35s/beat saved, zero production loss. 228 var nroots: i64 = SCAN_ROOTS 229 if argc >= 2 { nroots = 1 } 230 while ri < nroots { 231 findings = findings + ss_walk_root(roots[ri] as *u8, fbuf, tabs, ifd, files) 232 ri = ri + 1 233 } 234 sys_close(ifd) 235 236 let lfd: i64 = sys_openat_append("knowledge/status/secret_scan.log" as *u8, 0x1a4) 237 if lfd >= 0 { 238 _fp(lfd, "SECRET-SCAN epoch=" as *u8); _fn(lfd, sys_now_realtime_sec()) 239 _fp(lfd, " files=" as *u8); _fn(lfd, files[0]) 240 _fp(lfd, " findings=" as *u8); _fn(lfd, findings) 241 if findings == 0 { _fp(lfd, " verdict=GREEN\n" as *u8) } else { _fp(lfd, " verdict=RED\n" as *u8) } 242 sys_close(lfd) 243 } 244 _p("SECRET-SCAN files=" as *u8); _fn(1, files[0]) 245 _p(" findings=" as *u8); _fn(1, findings) 246 if findings == 0 { _p(" verdict=GREEN\n" as *u8); sys_exit(0); return 0 } 247 _p(" verdict=RED (each finding appended as PLAINTEXT-SECRET to issues_durable.log; sentinel will escalate)\n" as *u8) 248 sys_exit(1) 249 return 1 250}