1use serde::Serialize;
23
24#[derive(Debug, Clone, Serialize)]
26pub struct CweMutation {
27 pub safe_script: String,
29 pub unsafe_script: String,
31 pub cwe: String,
33 pub cwe_id: u32,
35 pub vulnerability: String,
37 pub mutation_description: String,
39}
40
41struct Xorshift64 {
46 state: u64,
47}
48
49impl Xorshift64 {
50 fn new(seed: u64) -> Self {
51 Self {
53 state: if seed == 0 {
54 0x5851_F42D_4C95_7F2D
55 } else {
56 seed
57 },
58 }
59 }
60
61 fn next(&mut self) -> u64 {
62 let mut s = self.state;
63 s ^= s << 13;
64 s ^= s >> 7;
65 s ^= s << 17;
66 self.state = s;
67 s
68 }
69
70 fn pick<'a, T>(&mut self, items: &'a [T]) -> &'a T {
72 let idx = (self.next() as usize) % items.len();
73 &items[idx]
74 }
75}
76
77struct BaseTemplate {
83 safe: &'static str,
84 vuln: &'static str,
85 desc: &'static str,
86}
87
88struct Substitution {
90 placeholder: &'static str,
91 values: &'static [&'static str],
92}
93
94fn expand_templates(
97 bases: &[BaseTemplate],
98 count: usize,
99 seed: u64,
100 subs: &[Substitution],
101 cwe_tag: &str,
102 cwe_id: u32,
103 vuln_name: &str,
104) -> Vec<CweMutation> {
105 let mut rng = Xorshift64::new(seed.wrapping_add(u64::from(cwe_id)));
106 let mut seen = std::collections::HashSet::new();
107 let mut results = Vec::with_capacity(count);
108
109 for base in bases {
112 if results.len() >= count {
113 break;
114 }
115 let safe = apply_subs(base.safe, subs, &mut rng);
116 let vuln = apply_subs(base.vuln, subs, &mut rng);
117 let desc = apply_subs(base.desc, subs, &mut rng);
118 let key = format!("{safe}||{vuln}");
119 if seen.insert(key) {
120 results.push(CweMutation {
121 safe_script: safe,
122 unsafe_script: vuln,
123 cwe: cwe_tag.to_string(),
124 cwe_id,
125 vulnerability: vuln_name.to_string(),
126 mutation_description: desc,
127 });
128 }
129 }
130
131 let mut attempts = 0u64;
133 while results.len() < count && attempts < (count as u64) * 20 {
134 attempts += 1;
135 let base = &bases[(rng.next() as usize) % bases.len()];
136 let safe = apply_subs(base.safe, subs, &mut rng);
137 let vuln = apply_subs(base.vuln, subs, &mut rng);
138 let desc = apply_subs(base.desc, subs, &mut rng);
139 let key = format!("{safe}||{vuln}");
140 if seen.insert(key) {
141 results.push(CweMutation {
142 safe_script: safe,
143 unsafe_script: vuln,
144 cwe: cwe_tag.to_string(),
145 cwe_id,
146 vulnerability: vuln_name.to_string(),
147 mutation_description: desc,
148 });
149 }
150 }
151
152 results.truncate(count);
153 results
154}
155
156fn apply_subs(text: &str, subs: &[Substitution], rng: &mut Xorshift64) -> String {
158 let mut result = text.to_string();
159 for sub in subs {
160 while result.contains(sub.placeholder) {
163 let val = rng.pick(sub.values);
164 result = result.replacen(sub.placeholder, val, 1);
165 }
166 }
167 result
168}
169
170pub fn generate_cwe_mutations(cwe_targets: &[u32], count: usize, seed: u64) -> Vec<CweMutation> {
178 let mut results = Vec::new();
179 let n = cwe_targets.len().max(1);
180 let base_per_cwe = count / n;
181 let remainder = count % n;
182
183 for (i, &cwe_id) in cwe_targets.iter().enumerate() {
184 let this_count = base_per_cwe + usize::from(i < remainder);
186 let mutations = match cwe_id {
187 78 => generate_cwe_78(this_count, seed),
188 94 => generate_cwe_94(this_count, seed),
189 330 => generate_cwe_330(this_count, seed),
190 362 => generate_cwe_362(this_count, seed),
191 798 => generate_cwe_798(this_count, seed),
192 829 => generate_cwe_829(this_count, seed),
193 377 => generate_cwe_377(this_count, seed),
194 732 => generate_cwe_732(this_count, seed),
195 426 => generate_cwe_426(this_count, seed),
197 77 => generate_cwe_77(this_count, seed),
198 116 => generate_cwe_116(this_count, seed),
199 250 => generate_cwe_250(this_count, seed),
200 _ => Vec::new(),
201 };
202 results.extend(mutations);
203 }
204
205 results.truncate(count);
206 results
207}
208
209pub fn in_distribution_cwes() -> Vec<u32> {
211 vec![78, 94, 330, 362, 377, 732, 798, 829]
212}
213
214pub fn ood_cwes() -> Vec<u32> {
216 vec![426, 77, 116, 250]
217}
218
219#[allow(clippy::too_many_lines)]
225fn generate_cwe_78(count: usize, seed: u64) -> Vec<CweMutation> {
226 static BASES: &[BaseTemplate] = &[
227 BaseTemplate {
228 safe: "#!/bin/sh\nrm -rf \"${dir:?}\"/tmp",
229 vuln: "#!/bin/sh\nrm -rf $dir/tmp",
230 desc: "Unquoted variable in rm command allows path injection",
231 },
232 BaseTemplate {
233 safe: "#!/bin/sh\ncp \"${src}\" \"${dst}\"",
234 vuln: "#!/bin/sh\ncp $src $dst",
235 desc: "Unquoted variables in cp allow argument injection",
236 },
237 BaseTemplate {
238 safe: "#!/bin/sh\nfind \"${search_dir}\" -name '*.log'",
239 vuln: "#!/bin/sh\nfind $search_dir -name '*.log'",
240 desc: "Unquoted variable in find allows path traversal",
241 },
242 BaseTemplate {
243 safe: "#!/bin/sh\ngrep \"${pattern}\" \"${file}\"",
244 vuln: "#!/bin/sh\ngrep $pattern $file",
245 desc: "Unquoted grep arguments allow injection",
246 },
247 BaseTemplate {
248 safe: "#!/bin/sh\nchmod 644 \"${target_file}\"",
249 vuln: "#!/bin/sh\nchmod 644 $target_file",
250 desc: "Unquoted variable in chmod allows arg injection",
251 },
252 BaseTemplate {
253 safe: "#!/bin/sh\nmv \"${old_name}\" \"${new_name}\"",
254 vuln: "#!/bin/sh\nmv $old_name $new_name",
255 desc: "Unquoted variables in mv allow injection",
256 },
257 BaseTemplate {
258 safe: "#!/bin/sh\ntar czf backup.tar.gz \"${backup_dir}\"",
259 vuln: "#!/bin/sh\ntar czf backup.tar.gz $backup_dir",
260 desc: "Unquoted variable in tar allows injection",
261 },
262 BaseTemplate {
263 safe: "#!/bin/sh\nssh \"${remote_host}\" 'uptime'",
264 vuln: "#!/bin/sh\nssh $remote_host 'uptime'",
265 desc: "Unquoted variable in ssh allows host injection",
266 },
267 BaseTemplate {
268 safe: "#!/bin/sh\ncat \"${logfile}\" | head -n 100",
269 vuln: "#!/bin/sh\ncat $logfile | head -n 100",
270 desc: "Unquoted variable in cat allows filename injection",
271 },
272 BaseTemplate {
273 safe: "#!/bin/sh\nwc -l \"${input_file}\"",
274 vuln: "#!/bin/sh\nwc -l $input_file",
275 desc: "Unquoted variable in wc allows argument injection",
276 },
277 BaseTemplate {
278 safe: "#!/bin/sh\nsort \"${data_file}\" > \"${output_file}\"",
279 vuln: "#!/bin/sh\nsort $data_file > $output_file",
280 desc: "Unquoted variables in sort/redirect allow injection",
281 },
282 BaseTemplate {
283 safe: "#!/bin/sh\nhead -n 10 \"${report}\"",
284 vuln: "#!/bin/sh\nhead -n 10 $report",
285 desc: "Unquoted variable in head allows filename injection",
286 },
287 BaseTemplate {
288 safe: "#!/bin/sh\ntouch \"${marker_file}\"",
289 vuln: "#!/bin/sh\ntouch $marker_file",
290 desc: "Unquoted variable in touch allows path injection",
291 },
292 BaseTemplate {
293 safe: "#!/bin/sh\nchown root:root \"${config_file}\"",
294 vuln: "#!/bin/sh\nchown root:root $config_file",
295 desc: "Unquoted variable in chown allows argument injection",
296 },
297 BaseTemplate {
298 safe: "#!/bin/sh\n{CMD} \"${VAR}\"",
299 vuln: "#!/bin/sh\n{CMD} ${VAR}",
300 desc: "Unquoted variable in {CMD} allows {ATTACK} injection",
301 },
302 ];
303
304 let subs = &[
305 Substitution {
306 placeholder: "{VAR}",
307 values: &[
308 "dir",
309 "path",
310 "file",
311 "input",
312 "output",
313 "src",
314 "dst",
315 "name",
316 "target",
317 "prefix",
318 "suffix",
319 "archive",
320 "config",
321 "data",
322 "log",
323 "report",
324 "backup",
325 "cache",
326 "temp",
327 "home_dir",
328 "work_dir",
329 "build_dir",
330 "deploy_dir",
331 "user_input",
332 "remote_path",
333 ],
334 },
335 Substitution {
336 placeholder: "{CMD}",
337 values: &[
338 "ls",
339 "cat",
340 "stat",
341 "file",
342 "du",
343 "df",
344 "tail",
345 "head",
346 "wc",
347 "md5sum",
348 "sha256sum",
349 "readlink",
350 "realpath",
351 "basename",
352 "dirname",
353 "test -f",
354 "test -d",
355 ],
356 },
357 Substitution {
358 placeholder: "{ATTACK}",
359 values: &["path", "argument", "filename", "glob", "command"],
360 },
361 ];
362
363 expand_templates(
364 BASES,
365 count,
366 seed,
367 subs,
368 "CWE-78",
369 78,
370 "OS Command Injection",
371 )
372}
373
374#[allow(clippy::too_many_lines)]
376fn generate_cwe_94(count: usize, seed: u64) -> Vec<CweMutation> {
377 static BASES: &[BaseTemplate] = &[
378 BaseTemplate {
379 safe: "#!/bin/sh\necho \"$user_input\"",
380 vuln: "#!/bin/sh\neval \"$user_input\"",
381 desc: "eval of user input allows arbitrary code execution",
382 },
383 BaseTemplate {
384 safe: "#!/bin/sh\nresult=$(echo \"$cmd_name\")",
385 vuln: "#!/bin/sh\nresult=$(eval \"$cmd_name\")",
386 desc: "eval in command substitution allows injection",
387 },
388 BaseTemplate {
389 safe: "#!/bin/sh\necho \"Setting config: ${key}=${value}\"",
390 vuln: "#!/bin/sh\neval \"${key}=${value}\"",
391 desc: "eval of config values allows code injection",
392 },
393 BaseTemplate {
394 safe: "#!/bin/sh\n. /etc/app/config.sh",
395 vuln: "#!/bin/sh\n. \"$CONFIG_PATH\"",
396 desc: "source of variable path allows code injection",
397 },
398 BaseTemplate {
399 safe: "#!/bin/sh\nexport {VAR}=\"${{{VAR}}}\"",
400 vuln: "#!/bin/sh\neval \"export {VAR}=${{1}}\"",
401 desc: "eval of export with positional param allows injection",
402 },
403 BaseTemplate {
404 safe: "#!/bin/sh\nprintf '%s\\n' \"${msg}\"",
405 vuln: "#!/bin/sh\neval echo \"${msg}\"",
406 desc: "eval echo allows command substitution injection",
407 },
408 BaseTemplate {
409 safe: "#!/bin/sh\ncase \"$action\" in\n start) do_start;;\n stop) do_stop;;\nesac",
410 vuln: "#!/bin/sh\neval \"do_$action\"",
411 desc: "eval of action name allows arbitrary function execution",
412 },
413 BaseTemplate {
414 safe: "#!/bin/sh\nawk -v pat=\"$pattern\" '$0 ~ pat' data.txt",
415 vuln: "#!/bin/sh\neval \"grep $pattern data.txt\"",
416 desc: "eval of grep pattern allows command injection",
417 },
418 BaseTemplate {
419 safe: "#!/bin/sh\nset -- \"$@\"\nfor arg do printf '%s\\n' \"$arg\"; done",
420 vuln: "#!/bin/sh\neval \"echo $*\"",
421 desc: "eval of $* allows injection via arguments",
422 },
423 BaseTemplate {
424 safe: "#!/bin/sh\nreadonly {VAR}=\"safe_value\"",
425 vuln: "#!/bin/sh\neval \"{VAR}=${{untrusted}}\"",
426 desc: "eval of variable assignment from untrusted source",
427 },
428 BaseTemplate {
429 safe: "#!/bin/sh\nif [ \"$mode\" = debug ]; then set -x; fi",
430 vuln: "#!/bin/sh\neval \"set $mode_flags\"",
431 desc: "eval of shell flags allows arbitrary option injection",
432 },
433 BaseTemplate {
434 safe: "#!/bin/sh\ncommand -v \"${tool}\" >/dev/null",
435 vuln: "#!/bin/sh\neval \"type ${tool}\" >/dev/null",
436 desc: "eval of type command allows injection via tool name",
437 },
438 BaseTemplate {
439 safe: "#!/bin/sh\n[ -f \"${path}\" ] && cat \"${path}\"",
440 vuln: "#!/bin/sh\neval \"cat ${path}\"",
441 desc: "eval of cat with variable path allows injection",
442 },
443 ];
444
445 let subs = &[Substitution {
446 placeholder: "{VAR}",
447 values: &[
448 "LANG", "MODE", "CONFIG", "OUTPUT", "FORMAT", "LEVEL", "PREFIX", "SUFFIX", "NAME",
449 "TYPE", "ENCODING", "STYLE",
450 ],
451 }];
452
453 expand_templates(BASES, count, seed, subs, "CWE-94", 94, "Code Injection")
454}
455
456#[allow(clippy::too_many_lines)]
458fn generate_cwe_330(count: usize, seed: u64) -> Vec<CweMutation> {
459 static BASES: &[BaseTemplate] = &[
460 BaseTemplate {
461 safe: "#!/bin/sh\ntmpdir=$(mktemp -d)",
462 vuln: "#!/bin/sh\ntmpdir=/tmp/work_$RANDOM",
463 desc: "$RANDOM is predictable (15-bit LFSR), use mktemp",
464 },
465 BaseTemplate {
466 safe: "#!/bin/sh\ntoken=$(head -c 32 /dev/urandom | base64)",
467 vuln: "#!/bin/sh\ntoken=session_$$",
468 desc: "PID-based token is predictable",
469 },
470 BaseTemplate {
471 safe: "#!/bin/sh\nlog_file=$(mktemp /tmp/log.XXXXXX)",
472 vuln: "#!/bin/sh\nlog_file=/tmp/log_$(date +%s)",
473 desc: "Timestamp-based filename is predictable",
474 },
475 BaseTemplate {
476 safe: "#!/bin/sh\nsalt=$(head -c 16 /dev/urandom | xxd -p)",
477 vuln: "#!/bin/sh\nsalt=$RANDOM$RANDOM",
478 desc: "Double $RANDOM is only 30 bits of entropy",
479 },
480 BaseTemplate {
481 safe: "#!/bin/sh\n{ITEM}=$(mktemp /tmp/{ITEM}.XXXXXX)",
482 vuln: "#!/bin/sh\n{ITEM}=/tmp/{ITEM}_$RANDOM",
483 desc: "$RANDOM-based {ITEM} filename is predictable",
484 },
485 BaseTemplate {
486 safe: "#!/bin/sh\nsession_id=$(head -c 24 /dev/urandom | base64)",
487 vuln: "#!/bin/sh\nsession_id=sess_$$_$RANDOM",
488 desc: "PID+RANDOM session ID has only ~47 bits of entropy",
489 },
490 BaseTemplate {
491 safe: "#!/bin/sh\nnonce=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \\n')",
492 vuln: "#!/bin/sh\nnonce=$(date +%s%N)",
493 desc: "Nanosecond timestamp as nonce is predictable",
494 },
495 BaseTemplate {
496 safe: "#!/bin/sh\nport=$(shuf -i 49152-65535 -n 1)",
497 vuln: "#!/bin/sh\nport=$((RANDOM + 1024))",
498 desc: "$RANDOM port selection is predictable and biased",
499 },
500 BaseTemplate {
501 safe: "#!/bin/sh\ncookie=$(head -c 32 /dev/urandom | base64 | tr -d '/+=')",
502 vuln: "#!/bin/sh\ncookie=cookie_$(date +%s)_$$",
503 desc: "Timestamp+PID cookie is trivially guessable",
504 },
505 BaseTemplate {
506 safe: "#!/bin/sh\niv=$(openssl rand -hex 16)",
507 vuln: "#!/bin/sh\niv=$(printf '%032x' $RANDOM)",
508 desc: "$RANDOM IV is cryptographically weak",
509 },
510 BaseTemplate {
511 safe: "#!/bin/sh\nbackup_suffix=$(head -c 4 /dev/urandom | xxd -p)",
512 vuln: "#!/bin/sh\nbackup_suffix=$(date +%H%M%S)",
513 desc: "Time-based backup suffix is predictable",
514 },
515 BaseTemplate {
516 safe: "#!/bin/sh\njob_id=$(uuidgen)",
517 vuln: "#!/bin/sh\njob_id=job_$$",
518 desc: "PID-based job ID is predictable and reused",
519 },
520 BaseTemplate {
521 safe: "#!/bin/sh\napi_nonce=$(openssl rand -base64 18)",
522 vuln: "#!/bin/sh\napi_nonce=$RANDOM$RANDOM$RANDOM",
523 desc: "Triple $RANDOM is only 45 bits of entropy",
524 },
525 BaseTemplate {
526 safe: "#!/bin/sh\nworkdir=$(mktemp -d /tmp/{ITEM}.XXXXXX)",
527 vuln: "#!/bin/sh\nworkdir=/tmp/{ITEM}_$$",
528 desc: "PID-based workdir name is predictable and races",
529 },
530 BaseTemplate {
531 safe: "#!/bin/sh\npasswd=$(head -c 18 /dev/urandom | base64)",
532 vuln: "#!/bin/sh\npasswd=pw_$(date +%s | md5sum | head -c 12)",
533 desc: "Hashing a timestamp does not add entropy",
534 },
535 ];
536
537 let subs = &[Substitution {
538 placeholder: "{ITEM}",
539 values: &[
540 "cache", "lock", "pid", "sock", "fifo", "spool", "data", "state", "run", "build",
541 "deploy", "stage", "test", "bench", "upload", "download", "archive", "snapshot",
542 "dump",
543 ],
544 }];
545
546 expand_templates(
547 BASES,
548 count,
549 seed,
550 subs,
551 "CWE-330",
552 330,
553 "Insufficient Randomness",
554 )
555}
556
557#[allow(clippy::too_many_lines)]
559fn generate_cwe_362(count: usize, seed: u64) -> Vec<CweMutation> {
560 static BASES: &[BaseTemplate] = &[
561 BaseTemplate {
562 safe: "#!/bin/sh\nmkdir -p \"${output_dir}\"",
563 vuln: "#!/bin/sh\nmkdir \"${output_dir}\"",
564 desc: "mkdir without -p fails if dir exists (not idempotent)",
565 },
566 BaseTemplate {
567 safe: "#!/bin/sh\nrm -f \"${lockfile}\"",
568 vuln: "#!/bin/sh\nrm \"${lockfile}\"",
569 desc: "rm without -f fails if file missing (not idempotent)",
570 },
571 BaseTemplate {
572 safe: "#!/bin/sh\nln -sf \"${target}\" \"${link_name}\"",
573 vuln: "#!/bin/sh\nln -s \"${target}\" \"${link_name}\"",
574 desc: "ln -s without -f fails if link exists",
575 },
576 BaseTemplate {
577 safe: "#!/bin/sh\nmkdir -p /var/run/app && chmod 755 /var/run/app",
578 vuln: "#!/bin/sh\nmkdir /var/run/app && chmod 755 /var/run/app",
579 desc: "mkdir without -p causes TOCTOU race on re-run",
580 },
581 BaseTemplate {
582 safe: "#!/bin/sh\nmkdir -p \"${DIR}/{SUBDIR}\"",
583 vuln: "#!/bin/sh\nmkdir \"${DIR}/{SUBDIR}\"",
584 desc: "mkdir without -p on {SUBDIR} fails if exists",
585 },
586 BaseTemplate {
587 safe: "#!/bin/sh\nrm -f \"${DIR}/{FNAME}\"",
588 vuln: "#!/bin/sh\nrm \"${DIR}/{FNAME}\"",
589 desc: "rm without -f on {FNAME} errors if absent",
590 },
591 BaseTemplate {
592 safe: "#!/bin/sh\nln -sf /usr/bin/{TOOL} /usr/local/bin/{TOOL}",
593 vuln: "#!/bin/sh\nln -s /usr/bin/{TOOL} /usr/local/bin/{TOOL}",
594 desc: "ln -s without -f for {TOOL} symlink fails on re-run",
595 },
596 BaseTemplate {
597 safe: "#!/bin/sh\ncp -f \"${src}\" \"${dst}\"",
598 vuln: "#!/bin/sh\nif [ ! -f \"${dst}\" ]; then cp \"${src}\" \"${dst}\"; fi",
599 desc: "TOCTOU: check-then-copy races with concurrent access",
600 },
601 BaseTemplate {
602 safe: "#!/bin/sh\nmkdir -p /opt/{APP}/data /opt/{APP}/logs /opt/{APP}/tmp",
603 vuln: "#!/bin/sh\nmkdir /opt/{APP}/data\nmkdir /opt/{APP}/logs\nmkdir /opt/{APP}/tmp",
604 desc: "Multiple mkdir without -p for {APP} not idempotent",
605 },
606 BaseTemplate {
607 safe: "#!/bin/sh\ninstall -d -m 755 \"${cache_dir}\"",
608 vuln: "#!/bin/sh\nif [ ! -d \"${cache_dir}\" ]; then mkdir \"${cache_dir}\"; chmod 755 \"${cache_dir}\"; fi",
609 desc: "TOCTOU: test -d races with concurrent dir creation",
610 },
611 BaseTemplate {
612 safe: "#!/bin/sh\nmv -f \"${src}\" \"${dst}\"",
613 vuln: "#!/bin/sh\nif [ ! -f \"${dst}\" ]; then mv \"${src}\" \"${dst}\"; fi",
614 desc: "TOCTOU: existence check before mv races on busy systems",
615 },
616 BaseTemplate {
617 safe: "#!/bin/sh\nmkdir -p \"$HOME/.config/{APP}\"",
618 vuln: "#!/bin/sh\nmkdir \"$HOME/.config/{APP}\"",
619 desc: "mkdir without -p for user config dir not idempotent",
620 },
621 BaseTemplate {
622 safe: "#!/bin/sh\nrm -rf \"${build_dir}\" && mkdir -p \"${build_dir}\"",
623 vuln: "#!/bin/sh\nif [ -d \"${build_dir}\" ]; then rm -r \"${build_dir}\"; fi\nmkdir \"${build_dir}\"",
624 desc: "Check-then-remove-then-create has double TOCTOU race",
625 },
626 ];
627
628 let subs = &[
629 Substitution {
630 placeholder: "{SUBDIR}",
631 values: &[
632 "logs", "tmp", "cache", "data", "run", "state", "spool", "backup", "build",
633 "deploy", "dist", "output", "staging",
634 ],
635 },
636 Substitution {
637 placeholder: "{FNAME}",
638 values: &[
639 "lockfile",
640 "pidfile",
641 "socket",
642 "marker",
643 "flag",
644 "stamp",
645 "token",
646 "sentinel",
647 "checkpoint",
648 "progress",
649 ],
650 },
651 Substitution {
652 placeholder: "{TOOL}",
653 values: &[
654 "python3", "node", "ruby", "gcc", "clang", "go", "rustc", "cargo", "npm", "pip",
655 "java", "perl",
656 ],
657 },
658 Substitution {
659 placeholder: "{APP}",
660 values: &[
661 "myapp",
662 "webapp",
663 "api",
664 "worker",
665 "scheduler",
666 "monitor",
667 "proxy",
668 "gateway",
669 "service",
670 "daemon",
671 "agent",
672 "collector",
673 ],
674 },
675 Substitution {
676 placeholder: "{DIR}",
677 values: &[
678 "/var/run",
679 "/var/tmp",
680 "/opt/app",
681 "/srv/data",
682 "/usr/local/share",
683 "/etc/app",
684 "/home/deploy",
685 ],
686 },
687 ];
688
689 expand_templates(
690 BASES,
691 count,
692 seed,
693 subs,
694 "CWE-362",
695 362,
696 "TOCTOU Race Condition",
697 )
698}
699
700#[allow(clippy::too_many_lines)]
702fn generate_cwe_798(count: usize, seed: u64) -> Vec<CweMutation> {
703 static BASES: &[BaseTemplate] = &[
704 BaseTemplate {
705 safe: "#!/bin/sh\nDB_PASS=\"${DB_PASSWORD:?}\"",
706 vuln: "#!/bin/sh\nDB_PASS=\"s3cret123\"",
707 desc: "Hard-coded database password",
708 },
709 BaseTemplate {
710 safe: "#!/bin/sh\nAPI_KEY=\"$(vault kv get -field=key secret/api)\"",
711 vuln: "#!/bin/sh\nAPI_KEY=\"sk-abc123def456\"",
712 desc: "Hard-coded API key",
713 },
714 BaseTemplate {
715 safe: "#!/bin/sh\ntoken=\"${GITHUB_TOKEN:?}\"",
716 vuln: "#!/bin/sh\ntoken=\"FAKE_TOKEN_xxxxxxxxxxxxxxxxxxxxxxxxxxxx\"",
717 desc: "Hard-coded GitHub token",
718 },
719 BaseTemplate {
720 safe: "#!/bin/sh\n{CRED_VAR}=\"${{{CRED_ENV}:?}}\"",
721 vuln: "#!/bin/sh\n{CRED_VAR}=\"{CRED_VALUE}\"",
722 desc: "Hard-coded {CRED_TYPE} credential",
723 },
724 BaseTemplate {
725 safe: "#!/bin/sh\naws_key=$(aws configure get aws_access_key_id)",
726 vuln: "#!/bin/sh\naws_key=\"FAKEKEYID00000EXAMPLE\"",
727 desc: "Hard-coded AWS access key ID",
728 },
729 BaseTemplate {
730 safe: "#!/bin/sh\nsecret=$(cat /run/secrets/db_password)",
731 vuln: "#!/bin/sh\nsecret=\"P@ssw0rd!\"",
732 desc: "Hard-coded secret instead of file-based secret",
733 },
734 BaseTemplate {
735 safe: "#!/bin/sh\nSMTP_PASS=\"$(pass show email/smtp)\"",
736 vuln: "#!/bin/sh\nSMTP_PASS=\"mailpass123\"",
737 desc: "Hard-coded SMTP password",
738 },
739 BaseTemplate {
740 safe: "#!/bin/sh\nSSH_KEY=\"${SSH_PRIVATE_KEY_PATH:?}\"",
741 vuln: "#!/bin/sh\nSSH_KEY=\"-----BEGIN RSA PRIVATE KEY-----\"",
742 desc: "Hard-coded SSH private key material",
743 },
744 BaseTemplate {
745 safe: "#!/bin/sh\nJWT_SECRET=\"$(vault kv get -field=jwt secret/auth)\"",
746 vuln: "#!/bin/sh\nJWT_SECRET=\"super_secret_jwt_key_123\"",
747 desc: "Hard-coded JWT signing secret",
748 },
749 BaseTemplate {
750 safe: "#!/bin/sh\nDB_URL=\"postgresql://$DB_USER:$DB_PASS@$DB_HOST/$DB_NAME\"",
751 vuln: "#!/bin/sh\nDB_URL=\"postgresql://admin:password@db.example.com/prod\"",
752 desc: "Hard-coded database connection string with credentials",
753 },
754 BaseTemplate {
755 safe: "#!/bin/sh\nSTRIPE_KEY=\"${STRIPE_SECRET_KEY:?}\"",
756 vuln: "#!/bin/sh\nSTRIPE_KEY=\"EXAMPLE_FAKE_KEY_not_a_real_key\"",
757 desc: "Hard-coded payment API key",
758 },
759 BaseTemplate {
760 safe: "#!/bin/sh\nENCRYPT_KEY=$(head -c 32 /dev/urandom | base64)",
761 vuln: "#!/bin/sh\nENCRYPT_KEY=\"aGVsbG93b3JsZDEyMzQ1Njc4OTA=\"",
762 desc: "Hard-coded encryption key",
763 },
764 BaseTemplate {
765 safe: "#!/bin/sh\nSLACK_WEBHOOK=\"${SLACK_WEBHOOK_URL:?}\"",
766 vuln: "#!/bin/sh\nSLACK_WEBHOOK=\"https://hooks.slack.com/services/T00/B00/xxxx\"",
767 desc: "Hard-coded Slack webhook URL",
768 },
769 BaseTemplate {
770 safe: "#!/bin/sh\nREDIS_PASS=\"${REDIS_PASSWORD:-}\"",
771 vuln: "#!/bin/sh\nREDIS_PASS=\"redis_secret_42\"",
772 desc: "Hard-coded Redis password",
773 },
774 ];
775
776 let subs = &[
777 Substitution {
778 placeholder: "{CRED_VAR}",
779 values: &[
780 "DB_PASS",
781 "API_TOKEN",
782 "SECRET_KEY",
783 "AUTH_TOKEN",
784 "OAUTH_SECRET",
785 "DEPLOY_KEY",
786 "ACCESS_TOKEN",
787 "SERVICE_KEY",
788 "MASTER_KEY",
789 "ROOT_PASS",
790 "ADMIN_TOKEN",
791 ],
792 },
793 Substitution {
794 placeholder: "{CRED_ENV}",
795 values: &[
796 "DB_PASSWORD",
797 "API_TOKEN_SECRET",
798 "SECRET_KEY_FILE",
799 "AUTH_TOKEN_VAR",
800 "OAUTH_CLIENT_SECRET",
801 "DEPLOY_KEY_PATH",
802 "ACCESS_TOKEN_ENV",
803 "SERVICE_KEY_REF",
804 ],
805 },
806 Substitution {
807 placeholder: "{CRED_VALUE}",
808 values: &[
809 "xoxb-1234-5678-abcdef",
810 "gho_xxxxxxxxxxxx",
811 "FAKE_PAY_KEY_abcdef",
812 "AIzaSyXXXXXXXXXXXXXXXXXXXXXXX",
813 "dop_v1_xxxxx",
814 "glpat-xxxxxxxxxxxxxxxxxxxx",
815 "npm_xxxxxxxxxxxx",
816 "pypi-AgEIcHlwaS5vcmc_xxxxx",
817 "FAKECLOUD1234EXAMPLE",
818 "FAKE_VCS_ABCDEFxxxxxxxxx",
819 "sq0atp-xxxxxxxxxxxxxxxx",
820 ],
821 },
822 Substitution {
823 placeholder: "{CRED_TYPE}",
824 values: &[
825 "database",
826 "API",
827 "OAuth",
828 "service",
829 "deployment",
830 "authentication",
831 "encryption",
832 "access",
833 ],
834 },
835 ];
836
837 expand_templates(
838 BASES,
839 count,
840 seed,
841 subs,
842 "CWE-798",
843 798,
844 "Hard-coded Credentials",
845 )
846}
847
848#[allow(clippy::too_many_lines)]
850fn generate_cwe_829(count: usize, seed: u64) -> Vec<CweMutation> {
851 static BASES: &[BaseTemplate] = &[
852 BaseTemplate {
853 safe: "#!/bin/sh\ncurl -fsSL https://example.com/install.sh -o /tmp/install.sh\nsha256sum -c /tmp/install.sh.sha256\nsh /tmp/install.sh",
854 vuln: "#!/bin/sh\ncurl -fsSL https://example.com/install.sh | sh",
855 desc: "Piping curl to shell bypasses integrity verification",
856 },
857 BaseTemplate {
858 safe: "#!/bin/sh\nwget -q https://example.com/setup.sh -O /tmp/setup.sh\nchmod +x /tmp/setup.sh\n/tmp/setup.sh",
859 vuln: "#!/bin/sh\nwget -q -O- https://example.com/setup.sh | bash",
860 desc: "Piping wget to bash allows MitM code execution",
861 },
862 BaseTemplate {
863 safe: "#!/bin/sh\ncurl -fsSL https://{DOMAIN}/{SCRIPT} -o /tmp/{SCRIPT}\nsha256sum -c /tmp/{SCRIPT}.sha256\nsh /tmp/{SCRIPT}",
864 vuln: "#!/bin/sh\ncurl -fsSL https://{DOMAIN}/{SCRIPT} | sh",
865 desc: "Piping curl|sh for {SCRIPT} from {DOMAIN} bypasses verification",
866 },
867 BaseTemplate {
868 safe: "#!/bin/sh\ncurl -fsSL {URL} -o /tmp/script.sh\ngpg --verify /tmp/script.sh.sig /tmp/script.sh\nsh /tmp/script.sh",
869 vuln: "#!/bin/sh\ncurl -fsSL {URL} | bash -s --",
870 desc: "Piping curl to bash -s bypasses GPG verification",
871 },
872 BaseTemplate {
873 safe: "#!/bin/sh\nwget -q {URL} -O /tmp/bootstrap.sh\nexpected_hash=\"abc123\"\nactual_hash=$(sha256sum /tmp/bootstrap.sh | cut -d' ' -f1)\n[ \"$expected_hash\" = \"$actual_hash\" ] && sh /tmp/bootstrap.sh",
874 vuln: "#!/bin/sh\nwget -q -O- {URL} | sh -s",
875 desc: "Piping wget to sh -s bypasses hash verification",
876 },
877 BaseTemplate {
878 safe: "#!/bin/sh\ncurl -fsSL https://{DOMAIN}/get-{TOOL}.sh -o /tmp/get-{TOOL}.sh\nchmod +x /tmp/get-{TOOL}.sh\n/tmp/get-{TOOL}.sh",
879 vuln: "#!/bin/sh\ncurl -fsSL https://{DOMAIN}/get-{TOOL}.sh | sudo sh",
880 desc: "Piping curl to sudo sh for {TOOL} is extremely dangerous",
881 },
882 BaseTemplate {
883 safe: "#!/bin/sh\ngit clone --depth 1 https://github.com/{REPO}.git /tmp/repo\ncd /tmp/repo && make install",
884 vuln: "#!/bin/sh\ncurl -fsSL https://raw.githubusercontent.com/{REPO}/main/install.sh | bash",
885 desc: "Piping raw GitHub content to bash bypasses repo integrity",
886 },
887 BaseTemplate {
888 safe: "#!/bin/sh\npip install {TOOL} --require-hashes -r requirements.txt",
889 vuln: "#!/bin/sh\ncurl -fsSL https://bootstrap.pypa.io/get-{TOOL}.py | python3",
890 desc: "Piping curl to python3 for {TOOL} bypasses package verification",
891 },
892 BaseTemplate {
893 safe: "#!/bin/sh\napt-get install -y {TOOL}",
894 vuln: "#!/bin/sh\ncurl -fsSL https://deb.{DOMAIN}/setup | sudo bash",
895 desc: "Piping setup script to sudo bash instead of using package manager",
896 },
897 BaseTemplate {
898 safe: "#!/bin/sh\ncurl -fsSL {URL} -o /tmp/agent.sh\nchmod +x /tmp/agent.sh\nsha256sum --check /tmp/agent.sh.sha256\n/tmp/agent.sh",
899 vuln: "#!/bin/sh\ncurl {URL} | sh",
900 desc: "Piping curl (without -f) to sh ignores HTTP errors and integrity",
901 },
902 BaseTemplate {
903 safe: "#!/bin/sh\nsnap install {TOOL}",
904 vuln: "#!/bin/sh\ncurl -sSL https://{DOMAIN}/install-{TOOL}.sh | sh -",
905 desc: "curl|sh install of {TOOL} instead of snap package",
906 },
907 BaseTemplate {
908 safe: "#!/bin/sh\nnpm install -g {TOOL}",
909 vuln: "#!/bin/sh\ncurl -fsSL https://raw.githubusercontent.com/{REPO}/main/install.sh | sh",
910 desc: "curl|sh for npm tool {TOOL} bypasses registry verification",
911 },
912 ];
913
914 let subs = &[
915 Substitution {
916 placeholder: "{DOMAIN}",
917 values: &[
918 "get.docker.com",
919 "install.python-poetry.org",
920 "raw.githubusercontent.com",
921 "sh.rustup.rs",
922 "get.helm.sh",
923 "deb.nodesource.com",
924 "rpm.nodesource.com",
925 "packages.gitlab.com",
926 "cli.github.com",
927 "apt.releases.hashicorp.com",
928 ],
929 },
930 Substitution {
931 placeholder: "{SCRIPT}",
932 values: &[
933 "install.sh",
934 "setup.sh",
935 "bootstrap.sh",
936 "init.sh",
937 "configure.sh",
938 "deploy.sh",
939 "update.sh",
940 "migrate.sh",
941 ],
942 },
943 Substitution {
944 placeholder: "{TOOL}",
945 values: &[
946 "docker",
947 "poetry",
948 "rustup",
949 "helm",
950 "nvm",
951 "rvm",
952 "sdkman",
953 "volta",
954 "deno",
955 "bun",
956 "terraform",
957 "kubectl",
958 ],
959 },
960 Substitution {
961 placeholder: "{REPO}",
962 values: &[
963 "nvm-sh/nvm",
964 "pyenv/pyenv",
965 "rbenv/rbenv",
966 "asdf-vm/asdf",
967 "junegunn/fzf",
968 "ohmyzsh/ohmyzsh",
969 ],
970 },
971 Substitution {
972 placeholder: "{URL}",
973 values: &[
974 "https://get.docker.com",
975 "https://install.python-poetry.org",
976 "https://sh.rustup.rs",
977 "https://get.helm.sh/helm-install.sh",
978 "https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh",
979 ],
980 },
981 ];
982
983 expand_templates(
984 BASES,
985 count,
986 seed,
987 subs,
988 "CWE-829",
989 829,
990 "Inclusion of Untrusted Functionality",
991 )
992}
993
994#[allow(clippy::too_many_lines)]
996fn generate_cwe_377(count: usize, seed: u64) -> Vec<CweMutation> {
997 static BASES: &[BaseTemplate] = &[
998 BaseTemplate {
999 safe: "#!/bin/sh\ntmp=$(mktemp)",
1000 vuln: "#!/bin/sh\ntmp=/tmp/myapp_tmp",
1001 desc: "Predictable temp file allows symlink attack",
1002 },
1003 BaseTemplate {
1004 safe: "#!/bin/sh\nworkdir=$(mktemp -d)",
1005 vuln: "#!/bin/sh\nworkdir=/tmp/workdir",
1006 desc: "Predictable temp directory allows pre-creation attack",
1007 },
1008 BaseTemplate {
1009 safe: "#!/bin/sh\n{ITEM}=$(mktemp /tmp/{ITEM}.XXXXXX)",
1010 vuln: "#!/bin/sh\n{ITEM}=/tmp/{ITEM}_file",
1011 desc: "Predictable {ITEM} temp file allows symlink race",
1012 },
1013 BaseTemplate {
1014 safe:
1015 "#!/bin/sh\ntmpdir=$(mktemp -d /tmp/{APP}.XXXXXX)\ntrap 'rm -rf \"$tmpdir\"' EXIT",
1016 vuln: "#!/bin/sh\ntmpdir=/tmp/{APP}\nmkdir -p \"$tmpdir\"",
1017 desc: "Predictable {APP} temp dir with no cleanup trap",
1018 },
1019 BaseTemplate {
1020 safe: "#!/bin/sh\noutput=$(mktemp)\n{CMD} > \"$output\"",
1021 vuln: "#!/bin/sh\noutput=/tmp/output.txt\n{CMD} > \"$output\"",
1022 desc: "Predictable output file for {CMD} allows data theft",
1023 },
1024 BaseTemplate {
1025 safe: "#!/bin/sh\nfifo=$(mktemp -u /tmp/fifo.XXXXXX)\nmkfifo -m 600 \"$fifo\"",
1026 vuln: "#!/bin/sh\nfifo=/tmp/myfifo\nmkfifo \"$fifo\"",
1027 desc: "Predictable FIFO name with default perms allows hijack",
1028 },
1029 BaseTemplate {
1030 safe: "#!/bin/sh\nsock=$(mktemp -u /tmp/sock.XXXXXX)\ntrap 'rm -f \"$sock\"' EXIT",
1031 vuln: "#!/bin/sh\nsock=/tmp/app.sock",
1032 desc: "Predictable socket path allows pre-bind attack",
1033 },
1034 BaseTemplate {
1035 safe: "#!/bin/sh\ncache=$(mktemp /tmp/cache.XXXXXX)\nchmod 600 \"$cache\"",
1036 vuln: "#!/bin/sh\ncache=/tmp/cache\ntouch \"$cache\"",
1037 desc: "Predictable cache file with default permissions",
1038 },
1039 BaseTemplate {
1040 safe:
1041 "#!/bin/sh\nlock=$(mktemp /tmp/lock.XXXXXX)\ntrap 'rm -f \"$lock\"' EXIT INT TERM",
1042 vuln: "#!/bin/sh\nlock=/tmp/app.lock",
1043 desc: "Predictable lock file allows denial of service",
1044 },
1045 BaseTemplate {
1046 safe:
1047 "#!/bin/sh\npipe=$(mktemp -u)\nmkfifo -m 600 \"$pipe\"\ntrap 'rm -f \"$pipe\"' EXIT",
1048 vuln: "#!/bin/sh\npipe=/tmp/datapipe\nmkfifo \"$pipe\"",
1049 desc: "Predictable named pipe allows data interception",
1050 },
1051 BaseTemplate {
1052 safe: "#!/bin/sh\nstaging=$(mktemp -d /tmp/deploy.XXXXXX)",
1053 vuln: "#!/bin/sh\nstaging=/tmp/deploy_staging",
1054 desc: "Predictable staging dir allows content substitution",
1055 },
1056 BaseTemplate {
1057 safe: "#!/bin/sh\nlog=$(mktemp /tmp/log.XXXXXX)\nexec > \"$log\" 2>&1",
1058 vuln: "#!/bin/sh\nlog=/tmp/script.log\nexec > \"$log\" 2>&1",
1059 desc: "Predictable log file allows output interception",
1060 },
1061 BaseTemplate {
1062 safe: "#!/bin/sh\ndump=$(mktemp /tmp/dump.XXXXXX)\n{CMD} > \"$dump\"",
1063 vuln: "#!/bin/sh\n{CMD} > /tmp/dump.out",
1064 desc: "Predictable dump file for {CMD} allows data theft",
1065 },
1066 ];
1067
1068 let subs = &[
1069 Substitution {
1070 placeholder: "{ITEM}",
1071 values: &[
1072 "report", "config", "state", "session", "data", "audit", "trace", "diff", "patch",
1073 "manifest", "index", "digest",
1074 ],
1075 },
1076 Substitution {
1077 placeholder: "{APP}",
1078 values: &[
1079 "myapp", "deploy", "builder", "tester", "monitor", "agent", "backup", "restore",
1080 "migrate", "updater",
1081 ],
1082 },
1083 Substitution {
1084 placeholder: "{CMD}",
1085 values: &[
1086 "pg_dump",
1087 "mysqldump",
1088 "tar czf -",
1089 "df -h",
1090 "ps aux",
1091 "env",
1092 "printenv",
1093 "ip addr",
1094 "netstat -tlnp",
1095 ],
1096 },
1097 ];
1098
1099 expand_templates(
1100 BASES,
1101 count,
1102 seed,
1103 subs,
1104 "CWE-377",
1105 377,
1106 "Insecure Temporary File",
1107 )
1108}
1109
1110#[allow(clippy::too_many_lines)]
1112fn generate_cwe_732(count: usize, seed: u64) -> Vec<CweMutation> {
1113 static BASES: &[BaseTemplate] = &[
1114 BaseTemplate {
1115 safe: "#!/bin/sh\nchmod 600 /etc/app/secrets.conf",
1116 vuln: "#!/bin/sh\nchmod 777 /etc/app/secrets.conf",
1117 desc: "World-writable permissions on secrets file",
1118 },
1119 BaseTemplate {
1120 safe: "#!/bin/sh\nchmod 700 /var/run/app",
1121 vuln: "#!/bin/sh\nchmod 666 /var/run/app",
1122 desc: "World-readable/writable runtime directory",
1123 },
1124 BaseTemplate {
1125 safe: "#!/bin/sh\nchmod 600 \"${SECRET_FILE}\"",
1126 vuln: "#!/bin/sh\nchmod 644 \"${SECRET_FILE}\"",
1127 desc: "World-readable permissions on secret file",
1128 },
1129 BaseTemplate {
1130 safe: "#!/bin/sh\ninstall -m 600 {FNAME} /etc/{APP}/",
1131 vuln: "#!/bin/sh\ncp {FNAME} /etc/{APP}/\nchmod 777 /etc/{APP}/{FNAME}",
1132 desc: "World-writable {FNAME} for {APP} allows tampering",
1133 },
1134 BaseTemplate {
1135 safe: "#!/bin/sh\nchmod 700 \"$HOME/.ssh\"",
1136 vuln: "#!/bin/sh\nchmod 755 \"$HOME/.ssh\"",
1137 desc: "SSH directory readable by others leaks key metadata",
1138 },
1139 BaseTemplate {
1140 safe: "#!/bin/sh\nchmod 600 \"$HOME/.ssh/id_rsa\"",
1141 vuln: "#!/bin/sh\nchmod 644 \"$HOME/.ssh/id_rsa\"",
1142 desc: "World-readable SSH private key",
1143 },
1144 BaseTemplate {
1145 safe: "#!/bin/sh\nchmod 640 /etc/{APP}/{FNAME}\nchown root:{APP} /etc/{APP}/{FNAME}",
1146 vuln: "#!/bin/sh\nchmod 666 /etc/{APP}/{FNAME}",
1147 desc: "World-writable {FNAME} for {APP} instead of group ownership",
1148 },
1149 BaseTemplate {
1150 safe: "#!/bin/sh\numask 077\ntouch /tmp/sensitive_output",
1151 vuln: "#!/bin/sh\numask 000\ntouch /tmp/sensitive_output",
1152 desc: "umask 000 creates world-writable files by default",
1153 },
1154 BaseTemplate {
1155 safe: "#!/bin/sh\ninstall -m 750 -d /var/log/{APP}",
1156 vuln: "#!/bin/sh\nmkdir /var/log/{APP}\nchmod 777 /var/log/{APP}",
1157 desc: "World-writable log directory for {APP}",
1158 },
1159 BaseTemplate {
1160 safe: "#!/bin/sh\nchmod 400 /etc/ssl/private/{FNAME}",
1161 vuln: "#!/bin/sh\nchmod 644 /etc/ssl/private/{FNAME}",
1162 desc: "World-readable TLS private key {FNAME}",
1163 },
1164 BaseTemplate {
1165 safe: "#!/bin/sh\nchmod 640 /etc/shadow",
1166 vuln: "#!/bin/sh\nchmod 644 /etc/shadow",
1167 desc: "World-readable shadow file exposes password hashes",
1168 },
1169 BaseTemplate {
1170 safe: "#!/bin/sh\nchmod 600 \"$HOME/.{APP}rc\"",
1171 vuln: "#!/bin/sh\nchmod 666 \"$HOME/.{APP}rc\"",
1172 desc: "World-writable {APP} config file allows manipulation",
1173 },
1174 BaseTemplate {
1175 safe: "#!/bin/sh\nchmod 755 /usr/local/bin/{TOOL}",
1176 vuln: "#!/bin/sh\nchmod 777 /usr/local/bin/{TOOL}",
1177 desc: "World-writable binary {TOOL} allows replacement",
1178 },
1179 ];
1180
1181 let subs = &[
1182 Substitution {
1183 placeholder: "{APP}",
1184 values: &[
1185 "myapp",
1186 "nginx",
1187 "postgres",
1188 "redis",
1189 "grafana",
1190 "prometheus",
1191 "jenkins",
1192 "vault",
1193 "consul",
1194 "nomad",
1195 "traefik",
1196 "caddy",
1197 ],
1198 },
1199 Substitution {
1200 placeholder: "{FNAME}",
1201 values: &[
1202 "secrets.conf",
1203 "credentials.yml",
1204 "tls.key",
1205 "auth.json",
1206 "db.conf",
1207 "api_keys.env",
1208 "token.pem",
1209 "password.txt",
1210 "cert.key",
1211 "config.ini",
1212 ],
1213 },
1214 Substitution {
1215 placeholder: "{TOOL}",
1216 values: &[
1217 "deploy.sh",
1218 "backup.sh",
1219 "healthcheck",
1220 "cron-job",
1221 "service-wrapper",
1222 "init-script",
1223 ],
1224 },
1225 ];
1226
1227 expand_templates(
1228 BASES,
1229 count,
1230 seed,
1231 subs,
1232 "CWE-732",
1233 732,
1234 "Incorrect Permission Assignment",
1235 )
1236}
1237
1238#[allow(clippy::too_many_lines)]
1244fn generate_cwe_426(count: usize, seed: u64) -> Vec<CweMutation> {
1245 static BASES: &[BaseTemplate] = &[
1246 BaseTemplate {
1247 safe: "#!/bin/sh\n/usr/bin/python3 script.py",
1248 vuln: "#!/bin/sh\npython3 script.py",
1249 desc: "Relative command path allows PATH hijacking",
1250 },
1251 BaseTemplate {
1252 safe: "#!/bin/sh\nPATH=/usr/bin:/bin\nexport PATH\ngcc -o app app.c",
1253 vuln: "#!/bin/sh\nPATH=.:$PATH\nexport PATH\ngcc -o app app.c",
1254 desc: "Current directory in PATH allows trojan binary execution",
1255 },
1256 BaseTemplate {
1257 safe: "#!/bin/sh\n/bin/ls /var/log",
1258 vuln: "#!/bin/sh\nls /var/log",
1259 desc: "Relative 'ls' can be hijacked via PATH manipulation",
1260 },
1261 BaseTemplate {
1262 safe: "#!/bin/sh\nLD_LIBRARY_PATH=/usr/lib\nexport LD_LIBRARY_PATH\n./app",
1263 vuln: "#!/bin/sh\nLD_LIBRARY_PATH=.:/usr/lib\nexport LD_LIBRARY_PATH\n./app",
1264 desc: "Current dir in LD_LIBRARY_PATH allows library hijack",
1265 },
1266 BaseTemplate {
1267 safe: "#!/bin/sh\n/usr/bin/{TOOL} {ARGS}",
1268 vuln: "#!/bin/sh\n{TOOL} {ARGS}",
1269 desc: "Relative {TOOL} path allows PATH hijacking",
1270 },
1271 BaseTemplate {
1272 safe: "#!/bin/sh\nPATH=/usr/bin:/usr/local/bin:/bin\nexport PATH\n{TOOL} {ARGS}",
1273 vuln: "#!/bin/sh\nPATH=/tmp:$PATH\nexport PATH\n{TOOL} {ARGS}",
1274 desc: "Writable /tmp in PATH allows trojan {TOOL}",
1275 },
1276 BaseTemplate {
1277 safe: "#!/bin/sh\ncommand -v /usr/bin/{TOOL} >/dev/null && /usr/bin/{TOOL} {ARGS}",
1278 vuln: "#!/bin/sh\nwhich {TOOL} && {TOOL} {ARGS}",
1279 desc: "'which' follows PATH order, may find trojan {TOOL}",
1280 },
1281 BaseTemplate {
1282 safe: "#!/bin/sh\nPATH=/usr/bin:/bin\nexport PATH\nLD_LIBRARY_PATH=/usr/lib:/lib\nexport LD_LIBRARY_PATH",
1283 vuln: "#!/bin/sh\n# PATH and LD_LIBRARY_PATH inherited from caller",
1284 desc: "Inherited PATH/LD_LIBRARY_PATH may contain untrusted directories",
1285 },
1286 BaseTemplate {
1287 safe: "#!/bin/sh\nPATH=/usr/bin:/bin exec /usr/bin/{TOOL}",
1288 vuln: "#!/bin/sh\nexec {TOOL}",
1289 desc: "exec without resetting PATH allows {TOOL} hijack",
1290 },
1291 BaseTemplate {
1292 safe: "#!/bin/sh\nenv -i PATH=/usr/bin:/bin HOME=\"$HOME\" /usr/bin/{TOOL}",
1293 vuln: "#!/bin/sh\nenv PATH=\".:$PATH\" {TOOL}",
1294 desc: "Passing dot-PATH through env for {TOOL}",
1295 },
1296 BaseTemplate {
1297 safe: "#!/bin/sh\nPYTHONPATH=/usr/lib/python3/dist-packages\nexport PYTHONPATH\npython3 app.py",
1298 vuln: "#!/bin/sh\nPYTHONPATH=.:$PYTHONPATH\nexport PYTHONPATH\npython3 app.py",
1299 desc: "Current dir in PYTHONPATH allows malicious module import",
1300 },
1301 BaseTemplate {
1302 safe: "#!/bin/sh\nNODE_PATH=/usr/lib/node_modules\nexport NODE_PATH\nnode app.js",
1303 vuln: "#!/bin/sh\nNODE_PATH=./node_modules:$NODE_PATH\nexport NODE_PATH\nnode app.js",
1304 desc: "Relative NODE_PATH allows malicious package injection",
1305 },
1306 BaseTemplate {
1307 safe: "#!/bin/sh\nPERL5LIB=/usr/share/perl5\nexport PERL5LIB\nperl script.pl",
1308 vuln: "#!/bin/sh\nPERL5LIB=.:$PERL5LIB\nexport PERL5LIB\nperl script.pl",
1309 desc: "Current dir in PERL5LIB allows malicious module loading",
1310 },
1311 ];
1312
1313 let subs = &[
1314 Substitution {
1315 placeholder: "{TOOL}",
1316 values: &[
1317 "gcc", "make", "git", "curl", "tar", "gzip", "python3", "node", "ruby", "perl",
1318 "java", "go", "rustc", "clang", "awk", "sed",
1319 ],
1320 },
1321 Substitution {
1322 placeholder: "{ARGS}",
1323 values: &[
1324 "--version",
1325 "-h",
1326 "input.txt",
1327 "-o output",
1328 "--config cfg.yml",
1329 "-f Makefile",
1330 "src/main.c",
1331 "app.py",
1332 "script.rb",
1333 ],
1334 },
1335 ];
1336
1337 expand_templates(
1338 BASES,
1339 count,
1340 seed,
1341 subs,
1342 "CWE-426",
1343 426,
1344 "Untrusted Search Path",
1345 )
1346}
1347
1348#[allow(clippy::too_many_lines)]
1350fn generate_cwe_77(count: usize, seed: u64) -> Vec<CweMutation> {
1351 static BASES: &[BaseTemplate] = &[
1352 BaseTemplate {
1353 safe: "#!/bin/sh\nfind . -name '*.txt' -print0 | xargs -0 rm",
1354 vuln: "#!/bin/sh\nfind . -name '*.txt' | xargs rm",
1355 desc: "xargs without -0 allows filename injection via newlines/spaces",
1356 },
1357 BaseTemplate {
1358 safe: "#!/bin/sh\nfind /tmp -type f -print0 | xargs -0 chmod 644",
1359 vuln: "#!/bin/sh\nfind /tmp -type f | xargs chmod 644",
1360 desc: "Missing -print0/-0 allows injection via crafted filenames",
1361 },
1362 BaseTemplate {
1363 safe: "#!/bin/sh\nprintf '%s\\0' \"$@\" | xargs -0 grep pattern",
1364 vuln: "#!/bin/sh\necho \"$@\" | xargs grep pattern",
1365 desc: "Word-splitting user args through xargs without null delimiter",
1366 },
1367 BaseTemplate {
1368 safe: "#!/bin/sh\nwhile IFS= read -r file; do\n process \"$file\"\ndone < filelist.txt",
1369 vuln: "#!/bin/sh\nfor file in $(cat filelist.txt); do\n process $file\ndone",
1370 desc: "Command substitution + unquoted var allows injection via filenames",
1371 },
1372 BaseTemplate {
1373 safe: "#!/bin/sh\nfind \"{DIR}\" -name '*.{EXT}' -print0 | xargs -0 {CMD}",
1374 vuln: "#!/bin/sh\nfind \"{DIR}\" -name '*.{EXT}' | xargs {CMD}",
1375 desc: "xargs without -0 on {EXT} files allows filename injection",
1376 },
1377 BaseTemplate {
1378 safe: "#!/bin/sh\nfind . -type f -print0 | xargs -0 -I{} cp {} backup/",
1379 vuln: "#!/bin/sh\nfind . -type f | xargs -I{} cp {} backup/",
1380 desc: "xargs -I without -0 allows injection via crafted filenames",
1381 },
1382 BaseTemplate {
1383 safe: "#!/bin/sh\nwhile IFS= read -r -d '' line; do\n echo \"$line\"\ndone < <(find . -print0)",
1384 vuln: "#!/bin/sh\nfor line in $(find .); do\n echo $line\ndone",
1385 desc: "for-in with find output splits on spaces and glob-expands",
1386 },
1387 BaseTemplate {
1388 safe: "#!/bin/sh\nfind . -name '*.{EXT}' -exec {CMD} {} +",
1389 vuln: "#!/bin/sh\n{CMD} $(find . -name '*.{EXT}')",
1390 desc: "Command substitution of find splits filenames on whitespace",
1391 },
1392 BaseTemplate {
1393 safe: "#!/bin/sh\nfind \"{DIR}\" -maxdepth 1 -print0 | xargs -0 ls -la",
1394 vuln: "#!/bin/sh\nls -la $(find \"{DIR}\" -maxdepth 1)",
1395 desc: "Command substitution of find in ls allows glob injection",
1396 },
1397 BaseTemplate {
1398 safe: "#!/bin/sh\nfind . -name '*.{EXT}' -print0 | xargs -0 wc -l",
1399 vuln: "#!/bin/sh\nwc -l $(find . -name '*.{EXT}')",
1400 desc: "Unquoted find substitution in wc allows filename injection",
1401 },
1402 BaseTemplate {
1403 safe: "#!/bin/sh\nfind . -type f -newer reference -print0 | xargs -0 {CMD}",
1404 vuln: "#!/bin/sh\n{CMD} $(find . -type f -newer reference)",
1405 desc: "Unquoted find in {CMD} splits filenames with spaces",
1406 },
1407 BaseTemplate {
1408 safe: "#!/bin/sh\nwhile IFS= read -r name; do\n process \"$name\"\ndone < names.txt",
1409 vuln: "#!/bin/sh\nprocess $(cat names.txt)",
1410 desc: "Unquoted cat substitution word-splits and glob-expands",
1411 },
1412 BaseTemplate {
1413 safe: "#!/bin/sh\nfind . -name '*.bak' -print0 | xargs -0 rm -f",
1414 vuln: "#!/bin/sh\nrm -f $(find . -name '*.bak')",
1415 desc: "Unquoted find in rm allows deletion of unintended files",
1416 },
1417 ];
1418
1419 let subs = &[
1420 Substitution {
1421 placeholder: "{DIR}",
1422 values: &[
1423 "/tmp",
1424 "/var/log",
1425 "/home/user",
1426 "/opt/data",
1427 "/srv/uploads",
1428 "/usr/local/share",
1429 "/var/spool",
1430 ],
1431 },
1432 Substitution {
1433 placeholder: "{EXT}",
1434 values: &[
1435 "log", "txt", "csv", "json", "xml", "yaml", "conf", "bak", "tmp", "dat",
1436 ],
1437 },
1438 Substitution {
1439 placeholder: "{CMD}",
1440 values: &[
1441 "rm",
1442 "chmod 644",
1443 "chown root",
1444 "gzip",
1445 "sha256sum",
1446 "wc -l",
1447 "head -1",
1448 "cat",
1449 "file",
1450 "stat",
1451 ],
1452 },
1453 ];
1454
1455 expand_templates(BASES, count, seed, subs, "CWE-77", 77, "Command Injection")
1456}
1457
1458#[allow(clippy::too_many_lines)]
1460fn generate_cwe_116(count: usize, seed: u64) -> Vec<CweMutation> {
1461 static BASES: &[BaseTemplate] = &[
1462 BaseTemplate {
1463 safe: "#!/bin/sh\nprintf '%s\\n' \"$user_input\" >> /var/log/app.log",
1464 vuln: "#!/bin/sh\necho $user_input >> /var/log/app.log",
1465 desc: "Unquoted echo allows log injection via newlines and escape sequences",
1466 },
1467 BaseTemplate {
1468 safe: "#!/bin/sh\nprintf 'User: %s Action: %s\\n' \"$user\" \"$action\" >> audit.log",
1469 vuln: "#!/bin/sh\necho \"User: $user Action: $action\" >> audit.log",
1470 desc: "Unvalidated user/action fields allow log forging",
1471 },
1472 BaseTemplate {
1473 safe: "#!/bin/sh\nprintf '%s\\n' \"${msg}\" | tee -a output.log",
1474 vuln: "#!/bin/sh\necho -e \"$msg\" | tee -a output.log",
1475 desc: "echo -e interprets escape sequences from untrusted input",
1476 },
1477 BaseTemplate {
1478 safe: "#!/bin/sh\nprintf '[%s] %s: %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$level\" \"$msg\" >> {LOGFILE}",
1479 vuln: "#!/bin/sh\necho \"[$(date)] $level: $msg\" >> {LOGFILE}",
1480 desc: "Unquoted log fields allow injection into {LOGFILE}",
1481 },
1482 BaseTemplate {
1483 safe: "#!/bin/sh\nprintf 'IP=%s Method=%s Path=%s\\n' \"$ip\" \"$method\" \"$path\" >> access.log",
1484 vuln: "#!/bin/sh\necho \"IP=$ip Method=$method Path=$path\" >> access.log",
1485 desc: "Unvalidated HTTP fields allow access log forging",
1486 },
1487 BaseTemplate {
1488 safe: "#!/bin/sh\nprintf '%s\\n' \"$hostname\" >> /var/log/{LOGFILE}",
1489 vuln: "#!/bin/sh\necho -e $hostname >> /var/log/{LOGFILE}",
1490 desc: "echo -e of hostname allows escape sequence injection in {LOGFILE}",
1491 },
1492 BaseTemplate {
1493 safe: "#!/bin/sh\nprintf 'ERROR: %s (code=%s)\\n' \"$errmsg\" \"$errcode\" >&2",
1494 vuln: "#!/bin/sh\necho \"ERROR: $errmsg (code=$errcode)\" >&2",
1495 desc: "Unvalidated error message allows stderr log injection",
1496 },
1497 BaseTemplate {
1498 safe: "#!/bin/sh\nprintf 'Event: %s\\n' \"${event}\" >> /var/log/{LOGFILE}",
1499 vuln: "#!/bin/sh\necho -e \"Event: ${event}\" >> /var/log/{LOGFILE}",
1500 desc: "echo -e of event data injects control chars into {LOGFILE}",
1501 },
1502 BaseTemplate {
1503 safe: "#!/bin/sh\nlogger -t myapp -- \"$msg\"",
1504 vuln: "#!/bin/sh\nlogger -t myapp $msg",
1505 desc: "Unquoted syslog message allows field splitting and injection",
1506 },
1507 BaseTemplate {
1508 safe: "#!/bin/sh\nprintf '%s|%s|%s\\n' \"$timestamp\" \"$user\" \"$query\" >> {LOGFILE}",
1509 vuln: "#!/bin/sh\necho \"$timestamp|$user|$query\" >> {LOGFILE}",
1510 desc: "Pipe-delimited log with unvalidated fields allows {LOGFILE} forging",
1511 },
1512 BaseTemplate {
1513 safe: "#!/bin/sh\nprintf '{\"user\":\"%s\",\"action\":\"%s\"}\\n' \"$user\" \"$action\" >> {LOGFILE}",
1514 vuln: "#!/bin/sh\necho '{\"user\":\"'$user'\",\"action\":\"'$action'\"}' >> {LOGFILE}",
1515 desc: "JSON log with unescaped fields allows structure injection",
1516 },
1517 BaseTemplate {
1518 safe: "#!/bin/sh\nprintf 'AUDIT: %s performed %s on %s\\n' \"$who\" \"$what\" \"$target\" >> {LOGFILE}",
1519 vuln: "#!/bin/sh\necho \"AUDIT: $who performed $what on $target\" >> {LOGFILE}",
1520 desc: "Unvalidated audit fields allow log record forgery",
1521 },
1522 BaseTemplate {
1523 safe: "#!/bin/sh\nprintf '%s\\n' \"$filename\" | sed 's/[^a-zA-Z0-9._-]/_/g'",
1524 vuln: "#!/bin/sh\necho $filename",
1525 desc: "Unsanitized filename echo allows terminal escape injection",
1526 },
1527 ];
1528
1529 let subs = &[Substitution {
1530 placeholder: "{LOGFILE}",
1531 values: &[
1532 "app.log",
1533 "audit.log",
1534 "access.log",
1535 "error.log",
1536 "auth.log",
1537 "security.log",
1538 "debug.log",
1539 "event.log",
1540 "transaction.log",
1541 "query.log",
1542 "activity.log",
1543 ],
1544 }];
1545
1546 expand_templates(
1547 BASES,
1548 count,
1549 seed,
1550 subs,
1551 "CWE-116",
1552 116,
1553 "Improper Output Encoding",
1554 )
1555}
1556
1557#[allow(clippy::too_many_lines)]
1559fn generate_cwe_250(count: usize, seed: u64) -> Vec<CweMutation> {
1560 static BASES: &[BaseTemplate] = &[
1561 BaseTemplate {
1562 safe: "#!/bin/sh\ninstall -m 644 config.conf /etc/app/",
1563 vuln: "#!/bin/sh\nsudo cp config.conf /etc/app/",
1564 desc: "Unnecessary sudo for file copy when install suffices",
1565 },
1566 BaseTemplate {
1567 safe: "#!/bin/sh\nsu -c 'systemctl restart app' appuser",
1568 vuln: "#!/bin/sh\nsudo systemctl restart app",
1569 desc: "Running as root when specific user suffices",
1570 },
1571 BaseTemplate {
1572 safe: "#!/bin/sh\nchown appuser:appgroup /var/run/app.pid",
1573 vuln: "#!/bin/sh\nsudo chmod 777 /var/run/app.pid",
1574 desc: "Using sudo + world-writable instead of proper ownership",
1575 },
1576 BaseTemplate {
1577 safe: "#!/bin/sh\ncap_add NET_BIND_SERVICE /usr/bin/app",
1578 vuln: "#!/bin/sh\nsudo /usr/bin/app",
1579 desc: "Running entire app as root instead of adding specific capability",
1580 },
1581 BaseTemplate {
1582 safe: "#!/bin/sh\nsu -c '{CMD}' {USER}",
1583 vuln: "#!/bin/sh\nsudo {CMD}",
1584 desc: "Unnecessary sudo for {CMD} when {USER} suffices",
1585 },
1586 BaseTemplate {
1587 safe: "#!/bin/sh\nrunuser -u {USER} -- {CMD}",
1588 vuln: "#!/bin/sh\nsudo -u root {CMD}",
1589 desc: "Running {CMD} as root instead of {USER}",
1590 },
1591 BaseTemplate {
1592 safe: "#!/bin/sh\nsetcap cap_net_bind_service=+ep /usr/bin/{TOOL}",
1593 vuln: "#!/bin/sh\nsudo /usr/bin/{TOOL}",
1594 desc: "Running {TOOL} as root instead of granting specific capability",
1595 },
1596 BaseTemplate {
1597 safe: "#!/bin/sh\ninstall -o {USER} -g {USER} -m 644 app.conf /etc/{APP}/",
1598 vuln: "#!/bin/sh\nsudo cp app.conf /etc/{APP}/\nsudo chmod 777 /etc/{APP}/app.conf",
1599 desc: "sudo + chmod 777 for {APP} config instead of proper install",
1600 },
1601 BaseTemplate {
1602 safe: "#!/bin/sh\nsudo -u {USER} {CMD}",
1603 vuln: "#!/bin/sh\nsudo {CMD}",
1604 desc: "Running {CMD} as root instead of least-privilege {USER}",
1605 },
1606 BaseTemplate {
1607 safe: "#!/bin/sh\nsu -c 'systemctl restart {APP}' {USER}",
1608 vuln: "#!/bin/sh\nsudo systemctl restart {APP}",
1609 desc: "Restarting {APP} as root instead of service user",
1610 },
1611 BaseTemplate {
1612 safe: "#!/bin/sh\nsetpriv --reuid={USER} --regid={USER} --clear-groups {CMD}",
1613 vuln: "#!/bin/sh\nsudo su -c '{CMD}'",
1614 desc: "sudo su chain for {CMD} instead of setpriv",
1615 },
1616 BaseTemplate {
1617 safe: "#!/bin/sh\nfind /var/log/{APP} -name '*.log' -mtime +30 -delete",
1618 vuln: "#!/bin/sh\nsudo find / -name '*.log' -mtime +30 -delete",
1619 desc: "sudo find from / instead of scoped log cleanup for {APP}",
1620 },
1621 BaseTemplate {
1622 safe: "#!/bin/sh\ncrontab -u {USER} -l | grep -q '{CMD}' || echo '0 2 * * * {CMD}' | crontab -u {USER} -",
1623 vuln: "#!/bin/sh\nsudo crontab -l | grep -q '{CMD}' || echo '0 2 * * * sudo {CMD}' | sudo crontab -",
1624 desc: "Root crontab with sudo instead of user-specific crontab",
1625 },
1626 ];
1627
1628 let subs = &[
1629 Substitution {
1630 placeholder: "{CMD}",
1631 values: &[
1632 "service nginx reload",
1633 "pg_dump mydb",
1634 "tail -f /var/log/app.log",
1635 "journalctl -u app",
1636 "kill -HUP $(cat /var/run/app.pid)",
1637 "logrotate /etc/logrotate.d/app",
1638 "certbot renew",
1639 ],
1640 },
1641 Substitution {
1642 placeholder: "{USER}",
1643 values: &[
1644 "appuser", "www-data", "nobody", "daemon", "postgres", "redis", "nginx", "deploy",
1645 "service", "monitor",
1646 ],
1647 },
1648 Substitution {
1649 placeholder: "{TOOL}",
1650 values: &[
1651 "nginx",
1652 "node",
1653 "python3",
1654 "java",
1655 "caddy",
1656 "envoy",
1657 "haproxy",
1658 "prometheus",
1659 ],
1660 },
1661 Substitution {
1662 placeholder: "{APP}",
1663 values: &[
1664 "myapp",
1665 "webapp",
1666 "api",
1667 "worker",
1668 "scheduler",
1669 "monitor",
1670 "proxy",
1671 "gateway",
1672 "backend",
1673 "frontend",
1674 ],
1675 },
1676 ];
1677
1678 expand_templates(
1679 BASES,
1680 count,
1681 seed,
1682 subs,
1683 "CWE-250",
1684 250,
1685 "Execution with Unnecessary Privileges",
1686 )
1687}
1688
1689#[cfg(test)]
1690mod tests {
1691 use super::*;
1692
1693 #[test]
1694 fn test_generate_cwe_78_mutations() {
1695 let mutations = generate_cwe_78(5, 42);
1696 assert!(!mutations.is_empty());
1697 assert!(mutations.len() <= 5);
1698 for m in &mutations {
1699 assert_eq!(m.cwe, "CWE-78");
1700 assert_eq!(m.cwe_id, 78);
1701 assert!(!m.safe_script.is_empty());
1702 assert!(!m.unsafe_script.is_empty());
1703 assert_ne!(m.safe_script, m.unsafe_script);
1704 }
1705 }
1706
1707 #[test]
1708 fn test_generate_cwe_94_mutations() {
1709 let mutations = generate_cwe_94(3, 42);
1710 assert!(!mutations.is_empty());
1711 for m in &mutations {
1712 assert_eq!(m.cwe, "CWE-94");
1713 assert!(m.unsafe_script.contains("eval") || m.unsafe_script.contains(". \"$"));
1714 }
1715 }
1716
1717 #[test]
1718 fn test_generate_ood_cwe_426() {
1719 let mutations = generate_cwe_426(4, 42);
1720 assert!(!mutations.is_empty());
1721 for m in &mutations {
1722 assert_eq!(m.cwe, "CWE-426");
1723 assert_eq!(m.cwe_id, 426);
1724 }
1725 }
1726
1727 #[test]
1728 fn test_generate_ood_cwe_77() {
1729 let mutations = generate_cwe_77(4, 42);
1730 assert!(!mutations.is_empty());
1731 for m in &mutations {
1732 assert_eq!(m.cwe, "CWE-77");
1733 }
1734 }
1735
1736 #[test]
1737 fn test_generate_ood_cwe_116() {
1738 let mutations = generate_cwe_116(3, 42);
1739 assert!(!mutations.is_empty());
1740 for m in &mutations {
1741 assert_eq!(m.cwe, "CWE-116");
1742 }
1743 }
1744
1745 #[test]
1746 fn test_generate_ood_cwe_250() {
1747 let mutations = generate_cwe_250(4, 42);
1748 assert!(!mutations.is_empty());
1749 for m in &mutations {
1750 assert_eq!(m.cwe, "CWE-250");
1751 }
1752 }
1753
1754 #[test]
1755 fn test_generate_cwe_mutations_mixed() {
1756 let mutations = generate_cwe_mutations(&[78, 94, 426, 77], 20, 42);
1757 assert!(!mutations.is_empty());
1758 assert!(mutations.len() <= 20);
1759 let cwes: std::collections::HashSet<&str> =
1761 mutations.iter().map(|m| m.cwe.as_str()).collect();
1762 assert!(cwes.len() > 1);
1763 }
1764
1765 #[test]
1766 fn test_in_distribution_cwes() {
1767 let ids = in_distribution_cwes();
1768 assert!(ids.contains(&78));
1769 assert!(ids.contains(&94));
1770 assert!(ids.contains(&330));
1771 assert!(ids.contains(&362));
1772 }
1773
1774 #[test]
1775 fn test_ood_cwes() {
1776 let ids = ood_cwes();
1777 assert!(ids.contains(&426));
1778 assert!(ids.contains(&77));
1779 assert!(ids.contains(&116));
1780 assert!(ids.contains(&250));
1781 }
1782
1783 #[test]
1784 fn test_ood_disjoint_from_in_distribution() {
1785 let id = in_distribution_cwes();
1786 let ood = ood_cwes();
1787 for cwe in &ood {
1788 assert!(
1789 !id.contains(cwe),
1790 "OOD CWE {cwe} should not be in-distribution"
1791 );
1792 }
1793 }
1794
1795 #[test]
1796 fn test_cwe_mutation_serialization() {
1797 let mutations = generate_cwe_78(1, 42);
1798 let json = serde_json::to_string(&mutations[0]).expect("serialize");
1799 assert!(json.contains("CWE-78"));
1800 assert!(json.contains("safe_script"));
1801 assert!(json.contains("unsafe_script"));
1802 }
1803
1804 #[test]
1807 fn test_xorshift64_deterministic() {
1808 let mut rng1 = Xorshift64::new(42);
1809 let mut rng2 = Xorshift64::new(42);
1810 for _ in 0..100 {
1811 assert_eq!(rng1.next(), rng2.next());
1812 }
1813 }
1814
1815 #[test]
1816 fn test_xorshift64_zero_seed() {
1817 let mut rng = Xorshift64::new(0);
1818 let v = rng.next();
1820 assert_ne!(v, 0);
1821 }
1822
1823 #[test]
1824 fn test_expand_templates_basic() {
1825 let bases = &[BaseTemplate {
1826 safe: "safe {X}",
1827 vuln: "vuln {X}",
1828 desc: "desc {X}",
1829 }];
1830 let subs = &[Substitution {
1831 placeholder: "{X}",
1832 values: &["alpha", "beta", "gamma"],
1833 }];
1834 let results = expand_templates(bases, 3, 42, subs, "CWE-999", 999, "Test");
1835 assert_eq!(results.len(), 3);
1836 for r in &results {
1837 assert!(!r.safe_script.contains("{X}"));
1838 assert!(!r.unsafe_script.contains("{X}"));
1839 }
1840 }
1841
1842 #[test]
1843 fn test_expand_templates_uniqueness() {
1844 let bases = &[BaseTemplate {
1845 safe: "safe {X} {Y}",
1846 vuln: "vuln {X} {Y}",
1847 desc: "desc",
1848 }];
1849 let subs = &[
1850 Substitution {
1851 placeholder: "{X}",
1852 values: &["a", "b", "c", "d", "e"],
1853 },
1854 Substitution {
1855 placeholder: "{Y}",
1856 values: &["1", "2", "3", "4", "5"],
1857 },
1858 ];
1859 let results = expand_templates(bases, 20, 42, subs, "CWE-999", 999, "Test");
1860 let mut seen = std::collections::HashSet::new();
1862 for r in &results {
1863 let key = format!("{}||{}", r.safe_script, r.unsafe_script);
1864 assert!(seen.insert(key), "Duplicate mutation found");
1865 }
1866 }
1867
1868 #[test]
1869 fn test_cwe_78_can_generate_100() {
1870 let mutations = generate_cwe_78(100, 42);
1871 assert!(
1872 mutations.len() >= 50,
1873 "Expected >=50 CWE-78 mutations, got {}",
1874 mutations.len()
1875 );
1876 }
1877
1878 #[test]
1879 fn test_cwe_94_can_generate_100() {
1880 let mutations = generate_cwe_94(100, 42);
1881 assert!(
1882 mutations.len() >= 30,
1883 "Expected >=30 CWE-94 mutations, got {}",
1884 mutations.len()
1885 );
1886 }
1887
1888 #[test]
1889 fn test_cwe_330_can_generate_100() {
1890 let mutations = generate_cwe_330(100, 42);
1891 assert!(
1892 mutations.len() >= 50,
1893 "Expected >=50 CWE-330 mutations, got {}",
1894 mutations.len()
1895 );
1896 }
1897
1898 #[test]
1899 fn test_cwe_362_can_generate_100() {
1900 let mutations = generate_cwe_362(100, 42);
1901 assert!(
1902 mutations.len() >= 50,
1903 "Expected >=50 CWE-362 mutations, got {}",
1904 mutations.len()
1905 );
1906 }
1907
1908 #[test]
1909 fn test_cwe_798_can_generate_100() {
1910 let mutations = generate_cwe_798(100, 42);
1911 assert!(
1912 mutations.len() >= 50,
1913 "Expected >=50 CWE-798 mutations, got {}",
1914 mutations.len()
1915 );
1916 }
1917
1918 #[test]
1919 fn test_cwe_829_can_generate_100() {
1920 let mutations = generate_cwe_829(100, 42);
1921 assert!(
1922 mutations.len() >= 50,
1923 "Expected >=50 CWE-829 mutations, got {}",
1924 mutations.len()
1925 );
1926 }
1927
1928 #[test]
1929 fn test_cwe_377_can_generate_100() {
1930 let mutations = generate_cwe_377(100, 42);
1931 assert!(
1932 mutations.len() >= 50,
1933 "Expected >=50 CWE-377 mutations, got {}",
1934 mutations.len()
1935 );
1936 }
1937
1938 #[test]
1939 fn test_cwe_732_can_generate_100() {
1940 let mutations = generate_cwe_732(100, 42);
1941 assert!(
1942 mutations.len() >= 50,
1943 "Expected >=50 CWE-732 mutations, got {}",
1944 mutations.len()
1945 );
1946 }
1947
1948 #[test]
1949 fn test_cwe_426_can_generate_100() {
1950 let mutations = generate_cwe_426(100, 42);
1951 assert!(
1952 mutations.len() >= 50,
1953 "Expected >=50 CWE-426 mutations, got {}",
1954 mutations.len()
1955 );
1956 }
1957
1958 #[test]
1959 fn test_cwe_77_can_generate_100() {
1960 let mutations = generate_cwe_77(100, 42);
1961 assert!(
1962 mutations.len() >= 50,
1963 "Expected >=50 CWE-77 mutations, got {}",
1964 mutations.len()
1965 );
1966 }
1967
1968 #[test]
1969 fn test_cwe_116_can_generate_100() {
1970 let mutations = generate_cwe_116(100, 42);
1971 assert!(
1972 mutations.len() >= 50,
1973 "Expected >=50 CWE-116 mutations, got {}",
1974 mutations.len()
1975 );
1976 }
1977
1978 #[test]
1979 fn test_cwe_250_can_generate_100() {
1980 let mutations = generate_cwe_250(100, 42);
1981 assert!(
1982 mutations.len() >= 50,
1983 "Expected >=50 CWE-250 mutations, got {}",
1984 mutations.len()
1985 );
1986 }
1987
1988 #[test]
1989 fn test_generate_all_cwes_500() {
1990 let all_cwes: Vec<u32> = {
1991 let mut v = in_distribution_cwes();
1992 v.extend(ood_cwes());
1993 v
1994 };
1995 let mutations = generate_cwe_mutations(&all_cwes, 500, 42);
1996 assert!(
1997 mutations.len() >= 400,
1998 "Expected >=400 total mutations, got {}",
1999 mutations.len()
2000 );
2001 }
2002
2003 #[test]
2004 fn test_deterministic_output() {
2005 let m1 = generate_cwe_78(50, 42);
2006 let m2 = generate_cwe_78(50, 42);
2007 assert_eq!(m1.len(), m2.len());
2008 for (a, b) in m1.iter().zip(m2.iter()) {
2009 assert_eq!(a.safe_script, b.safe_script);
2010 assert_eq!(a.unsafe_script, b.unsafe_script);
2011 }
2012 }
2013
2014 #[test]
2015 fn test_different_seeds_different_output() {
2016 let m1 = generate_cwe_78(50, 42);
2017 let m2 = generate_cwe_78(50, 99);
2018 let mut any_different = false;
2020 for (a, b) in m1.iter().zip(m2.iter()) {
2021 if a.safe_script != b.safe_script {
2022 any_different = true;
2023 break;
2024 }
2025 }
2026 assert!(
2027 any_different,
2028 "Different seeds should produce different output"
2029 );
2030 }
2031
2032 #[test]
2033 fn test_safe_unsafe_always_differ() {
2034 let all_cwes: Vec<u32> = {
2035 let mut v = in_distribution_cwes();
2036 v.extend(ood_cwes());
2037 v
2038 };
2039 let mutations = generate_cwe_mutations(&all_cwes, 200, 42);
2040 for m in &mutations {
2041 assert_ne!(
2042 m.safe_script, m.unsafe_script,
2043 "Safe and unsafe must differ for {} - {}",
2044 m.cwe, m.mutation_description
2045 );
2046 }
2047 }
2048
2049 #[test]
2050 fn test_all_mutations_have_shebang() {
2051 let all_cwes: Vec<u32> = {
2052 let mut v = in_distribution_cwes();
2053 v.extend(ood_cwes());
2054 v
2055 };
2056 let mutations = generate_cwe_mutations(&all_cwes, 200, 42);
2057 for m in &mutations {
2058 assert!(
2059 m.safe_script.starts_with("#!/bin/sh"),
2060 "Safe script missing shebang: {}",
2061 &m.safe_script[..m.safe_script.len().min(40)]
2062 );
2063 assert!(
2066 m.unsafe_script.starts_with("#!/bin/sh"),
2067 "Unsafe script missing shebang: {}",
2068 &m.unsafe_script[..m.unsafe_script.len().min(40)]
2069 );
2070 }
2071 }
2072}