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}