Skip to main content

verificar/mutator/
cwe_bash.rs

1//! CWE-targeted bash mutation for ShellSafetyBench
2//!
3//! Generates safe/unsafe shell script pairs with specific CWE patterns.
4//! Used by bashrs ShellSafetyBench (spec S14.9, Steps 7.2/7.2b).
5//!
6//! ## Supported CWEs
7//!
8//! **In-distribution** (covered by bashrs linter):
9//! - CWE-78: OS Command Injection (unquoted variables)
10//! - CWE-94: Code Injection (eval usage)
11//! - CWE-330: Insufficient Randomness ($RANDOM, $$)
12//! - CWE-362: TOCTOU Race Condition (missing -p/-f flags)
13//! - CWE-798: Hard-coded Credentials
14//! - CWE-829: Inclusion of Untrusted Functionality (curl|bash)
15//!
16//! **Out-of-distribution** (eval-only, not in bashrs linter):
17//! - CWE-426: Untrusted Search Path
18//! - CWE-77: Command Injection (xargs without -0)
19//! - CWE-116: Improper Output Encoding (log injection)
20//! - CWE-250: Execution with Unnecessary Privileges
21
22use serde::Serialize;
23
24/// A CWE-targeted mutation: safe version + mutated unsafe version
25#[derive(Debug, Clone, Serialize)]
26pub struct CweMutation {
27    /// Safe (original) script
28    pub safe_script: String,
29    /// Mutated (unsafe) script
30    pub unsafe_script: String,
31    /// CWE identifier (e.g., "CWE-78")
32    pub cwe: String,
33    /// CWE numeric ID
34    pub cwe_id: u32,
35    /// Human-readable vulnerability name
36    pub vulnerability: String,
37    /// Which mutation was applied
38    pub mutation_description: String,
39}
40
41// ---------------------------------------------------------------------------
42// Deterministic PRNG (xorshift64) — no external dependency needed
43// ---------------------------------------------------------------------------
44
45struct Xorshift64 {
46    state: u64,
47}
48
49impl Xorshift64 {
50    fn new(seed: u64) -> Self {
51        // Avoid zero-state (xorshift degenerates)
52        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    /// Pick a random element from a slice.
71    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
77// ---------------------------------------------------------------------------
78// Template expansion engine
79// ---------------------------------------------------------------------------
80
81/// A base template with placeholders like `{VAR}`, `{CMD}`, etc.
82struct BaseTemplate {
83    safe: &'static str,
84    vuln: &'static str,
85    desc: &'static str,
86}
87
88/// A substitution rule: placeholder string → list of possible values.
89struct Substitution {
90    placeholder: &'static str,
91    values: &'static [&'static str],
92}
93
94/// Expand base templates into `count` unique `CweMutation` entries by
95/// substituting placeholders with randomly-selected values.
96fn 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    // First pass: emit each base template once (identity substitution) to
110    // guarantee coverage even when count <= bases.len().
111    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    // Second pass: keep generating random variants until we have enough.
132    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
156/// Replace all `{PLACEHOLDER}` occurrences in `text` with random values.
157fn apply_subs(text: &str, subs: &[Substitution], rng: &mut Xorshift64) -> String {
158    let mut result = text.to_string();
159    for sub in subs {
160        // Replace each occurrence independently so the same placeholder in one
161        // template can get different values.
162        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
170// ---------------------------------------------------------------------------
171// Public API
172// ---------------------------------------------------------------------------
173
174/// Generate CWE-targeted mutations for bash scripts.
175///
176/// Returns pairs of (safe, unsafe) scripts for each CWE pattern.
177pub 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        // Distribute the remainder across the first `remainder` CWEs
185        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            // OOD CWEs
196            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
209/// All in-distribution CWE IDs (covered by bashrs linter)
210pub fn in_distribution_cwes() -> Vec<u32> {
211    vec![78, 94, 330, 362, 377, 732, 798, 829]
212}
213
214/// All out-of-distribution CWE IDs (eval-only)
215pub fn ood_cwes() -> Vec<u32> {
216    vec![426, 77, 116, 250]
217}
218
219// ===========================================================================
220// In-Distribution CWEs
221// ===========================================================================
222
223/// CWE-78: OS Command Injection via unquoted variables
224#[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/// CWE-94: Code Injection via eval
375#[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/// CWE-330: Insufficient Randomness ($RANDOM, $$)
457#[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/// CWE-362: TOCTOU Race Condition (missing -p/-f flags)
558#[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/// CWE-798: Hard-coded Credentials
701#[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/// CWE-829: Inclusion of Untrusted Functionality (curl|bash)
849#[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/// CWE-377: Insecure Temporary File
995#[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/// CWE-732: Incorrect Permission Assignment
1111#[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// ===========================================================================
1239// Out-of-Distribution CWEs (eval-only)
1240// ===========================================================================
1241
1242/// CWE-426: Untrusted Search Path
1243#[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/// CWE-77: Command Injection (xargs, indirect execution)
1349#[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/// CWE-116: Improper Output Encoding (log injection)
1459#[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/// CWE-250: Execution with Unnecessary Privileges
1558#[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        // Should have multiple CWE types
1760        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    // --- New tests for parameterized expansion ---
1805
1806    #[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        // Should not degenerate (non-zero state)
1819        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        // All entries should be unique
1861        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        // After the first ~14 base templates (which are deterministic), variants should differ
2019        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            // Some unsafe scripts intentionally have comment-only bodies
2064            // but should still start with shebang
2065            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}