1use serde::{Deserialize, Serialize};
18
19#[derive(Clone, Debug, Default, Serialize, Deserialize)]
21pub struct AnalysisProfile {
22 #[serde(default)]
24 pub profile: ProfileMeta,
25
26 #[serde(default)]
28 pub sources: Vec<TaintSource>,
29
30 #[serde(default)]
32 pub sinks: Vec<TaintSink>,
33
34 #[serde(default)]
36 pub transforms: Vec<TaintTransform>,
37
38 #[serde(default)]
40 pub sanitizers: Vec<TaintSanitizer>,
41}
42
43#[derive(Clone, Debug, Default, Serialize, Deserialize)]
45pub struct ProfileMeta {
46 pub name: String,
47 #[serde(default)]
48 pub description: String,
49 #[serde(default)]
50 pub version: String,
51 #[serde(default)]
52 pub author: String,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize)]
57pub struct TaintSource {
58 pub api: String,
60
61 pub taint_id: u32,
63
64 #[serde(default = "default_extract")]
66 pub extract: String,
67
68 #[serde(default)]
70 pub description: String,
71}
72
73#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct TaintSink {
76 pub api: String,
78
79 #[serde(default)]
81 pub dangerous_arg: u32,
82
83 #[serde(default = "default_severity")]
85 pub severity: String,
86
87 #[serde(default)]
89 pub cwe: String,
90
91 #[serde(default)]
93 pub description: String,
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize)]
98pub struct TaintTransform {
99 pub api: String,
101
102 #[serde(default = "default_true")]
104 pub propagates_taint: bool,
105}
106
107#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct TaintSanitizer {
110 pub api: String,
112
113 #[serde(default)]
115 pub kills_taint: Vec<String>,
116}
117
118fn default_extract() -> String {
119 "args[0]".into()
120}
121fn default_severity() -> String {
122 "high".into()
123}
124fn default_true() -> bool {
125 true
126}
127
128impl AnalysisProfile {
129 pub fn parse(toml_str: &str) -> Result<Self, String> {
131 toml::from_str(toml_str).map_err(|e| format!("profile parse error: {e}"))
132 }
133
134 pub fn merge(profiles: Vec<AnalysisProfile>) -> Result<Self, String> {
136 let mut merged = AnalysisProfile::default();
137 merged.profile.name = "merged".into();
138
139 let mut seen_taint_ids = std::collections::HashSet::new();
140
141 for profile in profiles {
142 for source in &profile.sources {
143 if !seen_taint_ids.insert(source.taint_id) {
144 return Err(format!(
145 "duplicate taint_id {} in source '{}' from profile '{}'",
146 source.taint_id, source.api, profile.profile.name,
147 ));
148 }
149 }
150
151 merged.sources.extend(profile.sources);
152 merged.sinks.extend(profile.sinks);
153 merged.transforms.extend(profile.transforms);
154 merged.sanitizers.extend(profile.sanitizers);
155 }
156
157 Ok(merged)
158 }
159
160 pub fn is_sink(&self, api: &str) -> Option<&TaintSink> {
162 self.sinks.iter().find(|s| s.api == api)
163 }
164
165 pub fn is_source(&self, api: &str) -> Option<&TaintSource> {
167 self.sources.iter().find(|s| s.api == api)
168 }
169
170 pub fn kills_taint(&self, api: &str, category: &str) -> bool {
172 self.sanitizers
173 .iter()
174 .any(|s| s.api == api && s.kills_taint.iter().any(|c| c == category))
175 }
176
177 pub fn rule_count(&self) -> usize {
179 self.sources.len() + self.sinks.len() + self.transforms.len() + self.sanitizers.len()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 const SAMPLE_PROFILE: &str = r#"
188[profile]
189name = "test-xss"
190description = "Test XSS detection"
191
192[[sources]]
193api = "chrome.runtime.onMessage"
194taint_id = 1
195extract = "args[0]"
196
197[[sources]]
198api = "window.postMessage"
199taint_id = 2
200extract = "event.data"
201
202[[sinks]]
203api = "chrome.tabs.executeScript"
204dangerous_arg = 1
205severity = "critical"
206cwe = "CWE-94"
207
208[[sinks]]
209api = "eval"
210dangerous_arg = 0
211severity = "critical"
212cwe = "CWE-95"
213
214[[sinks]]
215api = "element.innerHTML"
216dangerous_arg = 0
217severity = "high"
218cwe = "CWE-79"
219
220[[transforms]]
221api = "JSON.parse"
222propagates_taint = true
223
224[[sanitizers]]
225api = "DOMPurify.sanitize"
226kills_taint = ["xss"]
227"#;
228
229 #[test]
234 fn parse_profile() {
235 let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
236 assert_eq!(profile.profile.name, "test-xss");
237 assert_eq!(profile.profile.description, "Test XSS detection");
238 assert_eq!(profile.sources.len(), 2);
239 assert_eq!(profile.sinks.len(), 3);
240 assert_eq!(profile.transforms.len(), 1);
241 assert_eq!(profile.sanitizers.len(), 1);
242 }
243
244 #[test]
245 fn is_sink_lookup() {
246 let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
247 let sink = profile.is_sink("eval").unwrap();
248 assert_eq!(sink.cwe, "CWE-95");
249 assert_eq!(sink.severity, "critical");
250 assert!(profile.is_sink("chrome.tabs.query").is_none());
251 }
252
253 #[test]
254 fn is_source_lookup() {
255 let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
256 let source = profile.is_source("chrome.runtime.onMessage").unwrap();
257 assert_eq!(source.taint_id, 1);
258 assert!(profile.is_source("chrome.storage.get").is_none());
259 }
260
261 #[test]
262 fn kills_taint() {
263 let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
264 assert!(profile.kills_taint("DOMPurify.sanitize", "xss"));
265 assert!(!profile.kills_taint("DOMPurify.sanitize", "sqli"));
266 assert!(!profile.kills_taint("JSON.parse", "xss"));
267 }
268
269 #[test]
270 fn merge_profiles() {
271 let p1 = AnalysisProfile::parse(
272 r#"
273[profile]
274name = "p1"
275[[sources]]
276api = "source1"
277taint_id = 1
278[[sinks]]
279api = "sink1"
280"#,
281 )
282 .unwrap();
283
284 let p2 = AnalysisProfile::parse(
285 r#"
286[profile]
287name = "p2"
288[[sources]]
289api = "source2"
290taint_id = 2
291[[sinks]]
292api = "sink2"
293"#,
294 )
295 .unwrap();
296
297 let merged = AnalysisProfile::merge(vec![p1, p2]).unwrap();
298 assert_eq!(merged.sources.len(), 2);
299 assert_eq!(merged.sinks.len(), 2);
300 }
301
302 #[test]
303 fn merge_rejects_duplicate_taint_ids() {
304 let p1 = AnalysisProfile::parse(
305 r#"
306[profile]
307name = "p1"
308[[sources]]
309api = "source1"
310taint_id = 1
311"#,
312 )
313 .unwrap();
314
315 let p2 = AnalysisProfile::parse(
316 r#"
317[profile]
318name = "p2"
319[[sources]]
320api = "source2"
321taint_id = 1
322"#,
323 )
324 .unwrap();
325
326 assert!(AnalysisProfile::merge(vec![p1, p2]).is_err());
327 }
328
329 #[test]
330 fn rule_count() {
331 let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
332 assert_eq!(profile.rule_count(), 2 + 3 + 1 + 1); }
334
335 #[test]
336 fn empty_profile() {
337 let profile = AnalysisProfile::parse("[profile]\nname = \"empty\"").unwrap();
338 assert_eq!(profile.rule_count(), 0);
339 assert!(profile.is_sink("anything").is_none());
340 }
341
342 #[test]
347 fn parse_empty_profile_no_sources() {
348 let toml = r#"
349[profile]
350name = "no-sources"
351[[sinks]]
352api = "sink1"
353"#;
354 let profile = AnalysisProfile::parse(toml).unwrap();
355 assert!(profile.sources.is_empty());
356 assert_eq!(profile.sinks.len(), 1);
357 }
358
359 #[test]
360 fn parse_empty_profile_no_sinks() {
361 let toml = r#"
362[profile]
363name = "no-sinks"
364[[sources]]
365api = "source1"
366taint_id = 1
367"#;
368 let profile = AnalysisProfile::parse(toml).unwrap();
369 assert_eq!(profile.sources.len(), 1);
370 assert!(profile.sinks.is_empty());
371 }
372
373 #[test]
374 fn parse_empty_profile_no_sanitizers() {
375 let toml = r#"
376[profile]
377name = "no-sanitizers"
378[[sources]]
379api = "source1"
380taint_id = 1
381[[sinks]]
382api = "sink1"
383"#;
384 let profile = AnalysisProfile::parse(toml).unwrap();
385 assert!(profile.sanitizers.is_empty());
386 }
387
388 #[test]
389 fn parse_empty_profile_only_sources() {
390 let toml = r#"
391[profile]
392name = "only-sources"
393[[sources]]
394api = "source1"
395taint_id = 1
396[[sources]]
397api = "source2"
398taint_id = 2
399"#;
400 let profile = AnalysisProfile::parse(toml).unwrap();
401 assert_eq!(profile.sources.len(), 2);
402 assert!(profile.sinks.is_empty());
403 assert!(profile.transforms.is_empty());
404 assert!(profile.sanitizers.is_empty());
405 }
406
407 #[test]
408 fn parse_empty_profile_only_sinks() {
409 let toml = r#"
410[profile]
411name = "only-sinks"
412[[sinks]]
413api = "sink1"
414[[sinks]]
415api = "sink2"
416"#;
417 let profile = AnalysisProfile::parse(toml).unwrap();
418 assert!(profile.sources.is_empty());
419 assert_eq!(profile.sinks.len(), 2);
420 }
421
422 #[test]
423 fn parse_empty_profile_only_sanitizers() {
424 let toml = r#"
425[profile]
426name = "only-sanitizers"
427[[sanitizers]]
428api = "sanitizer1"
429kills_taint = ["xss"]
430"#;
431 let profile = AnalysisProfile::parse(toml).unwrap();
432 assert!(profile.sources.is_empty());
433 assert!(profile.sinks.is_empty());
434 assert_eq!(profile.sanitizers.len(), 1);
435 }
436
437 #[test]
438 fn parse_profile_only_transforms() {
439 let toml = r#"
440[profile]
441name = "only-transforms"
442[[transforms]]
443api = "transform1"
444propagates_taint = false
445"#;
446 let profile = AnalysisProfile::parse(toml).unwrap();
447 assert!(profile.sources.is_empty());
448 assert!(profile.sinks.is_empty());
449 assert_eq!(profile.transforms.len(), 1);
450 assert!(profile.sanitizers.is_empty());
451 }
452
453 #[test]
458 fn parse_invalid_toml_syntax() {
459 let toml = r#"
460[profile
461name = "broken"
462"#;
463 let result = AnalysisProfile::parse(toml);
464 assert!(result.is_err());
465 assert!(result.unwrap_err().contains("parse error"));
466 }
467
468 #[test]
469 fn parse_toml_missing_bracket() {
470 let toml = r#"
471[profile]
472name = "test"
473[[sources
474api = "source1"
475taint_id = 1
476"#;
477 let result = AnalysisProfile::parse(toml);
478 assert!(result.is_err());
479 }
480
481 #[test]
482 fn parse_toml_wrong_type_for_taint_id() {
483 let toml = r#"
485[profile]
486name = "test"
487[[sources]]
488api = "source1"
489taint_id = "not-a-number"
490"#;
491 let result = AnalysisProfile::parse(toml);
492 assert!(result.is_err());
493 }
494
495 #[test]
496 fn parse_toml_wrong_type_for_dangerous_arg() {
497 let toml = r#"
498[profile]
499name = "test"
500[[sinks]]
501api = "sink1"
502dangerous_arg = "not-a-number"
503"#;
504 let result = AnalysisProfile::parse(toml);
505 assert!(result.is_err());
506 }
507
508 #[test]
509 fn parse_toml_wrong_type_for_propagates_taint() {
510 let toml = r#"
511[profile]
512name = "test"
513[[transforms]]
514api = "transform1"
515propagates_taint = "not-a-boolean"
516"#;
517 let result = AnalysisProfile::parse(toml);
518 assert!(result.is_err());
519 }
520
521 #[test]
522 fn parse_toml_empty_string() {
523 let result = AnalysisProfile::parse("");
525 assert!(result.is_ok());
527 let profile = result.unwrap();
528 assert_eq!(profile.rule_count(), 0);
529 }
530
531 #[test]
532 fn parse_toml_whitespace_only() {
533 let result = AnalysisProfile::parse(" \n\t ");
535 assert!(result.is_ok());
536 let profile = result.unwrap();
537 assert_eq!(profile.rule_count(), 0);
538 }
539
540 #[test]
541 fn parse_toml_duplicate_table() {
542 let toml = r#"
544[profile]
545name = "test"
546[profile]
547name = "duplicate"
548"#;
549 let result = AnalysisProfile::parse(toml);
550 let _ = result;
553 }
554
555 #[test]
560 fn merge_five_profiles_no_conflict() {
561 let profiles: Vec<AnalysisProfile> = (1..=5)
562 .map(|i| {
563 AnalysisProfile::parse(&format!(
564 r#"
565[profile]
566name = "p{0}"
567[[sources]]
568api = "source{0}"
569taint_id = {0}
570[[sinks]]
571api = "sink{0}"
572"#,
573 i
574 ))
575 .unwrap()
576 })
577 .collect();
578
579 let merged = AnalysisProfile::merge(profiles).unwrap();
580 assert_eq!(merged.sources.len(), 5);
581 assert_eq!(merged.sinks.len(), 5);
582 }
583
584 #[test]
585 fn merge_duplicate_taint_id_first_position() {
586 let p1 = AnalysisProfile::parse(
587 r#"
588[profile]
589name = "p1"
590[[sources]]
591api = "source1"
592taint_id = 1
593"#,
594 )
595 .unwrap();
596
597 let p2 = AnalysisProfile::parse(
598 r#"
599[profile]
600name = "p2"
601[[sources]]
602api = "source2"
603taint_id = 1
604"#,
605 )
606 .unwrap();
607
608 let result = AnalysisProfile::merge(vec![p1, p2]);
609 assert!(result.is_err());
610 let err = result.unwrap_err();
611 assert!(err.contains("duplicate taint_id 1"));
612 assert!(err.contains("source2")); assert!(err.contains("p2")); }
615
616 #[test]
617 fn merge_duplicate_taint_id_middle() {
618 let p1 = AnalysisProfile::parse(
619 r#"
620[profile]
621name = "p1"
622[[sources]]
623api = "source1"
624taint_id = 1
625"#,
626 )
627 .unwrap();
628
629 let p2 = AnalysisProfile::parse(
630 r#"
631[profile]
632name = "p2"
633[[sources]]
634api = "source2"
635taint_id = 2
636"#,
637 )
638 .unwrap();
639
640 let p3 = AnalysisProfile::parse(
641 r#"
642[profile]
643name = "p3"
644[[sources]]
645api = "source3"
646taint_id = 2
647"#,
648 )
649 .unwrap();
650
651 let result = AnalysisProfile::merge(vec![p1, p2, p3]);
652 assert!(result.is_err());
653 }
654
655 #[test]
656 fn merge_duplicate_taint_id_last_position() {
657 let profiles: Vec<AnalysisProfile> = (1..=5)
658 .map(|i| {
659 AnalysisProfile::parse(&format!(
660 r#"
661[profile]
662name = "p{0}"
663[[sources]]
664api = "source{0}"
665taint_id = {0}
666"#,
667 i
668 ))
669 .unwrap()
670 })
671 .collect();
672
673 let p6 = AnalysisProfile::parse(
674 r#"
675[profile]
676name = "p6"
677[[sources]]
678api = "source6"
679taint_id = 5
680"#,
681 )
682 .unwrap();
683
684 let mut all_profiles = profiles;
685 all_profiles.push(p6);
686
687 let result = AnalysisProfile::merge(all_profiles);
688 assert!(result.is_err());
689 }
690
691 #[test]
692 fn merge_empty_vec() {
693 let merged = AnalysisProfile::merge(vec![]).unwrap();
694 assert_eq!(merged.rule_count(), 0);
695 assert_eq!(merged.profile.name, "merged");
696 }
697
698 #[test]
699 fn merge_single_profile() {
700 let p1 = AnalysisProfile::parse(
701 r#"
702[profile]
703name = "p1"
704[[sources]]
705api = "source1"
706taint_id = 1
707[[sinks]]
708api = "sink1"
709"#,
710 )
711 .unwrap();
712
713 let merged = AnalysisProfile::merge(vec![p1]).unwrap();
714 assert_eq!(merged.sources.len(), 1);
715 assert_eq!(merged.sinks.len(), 1);
716 }
717
718 #[test]
719 fn merge_preserves_all_sinks() {
720 let p1 = AnalysisProfile::parse(
721 r#"
722[[sinks]]
723api = "sink1"
724[[sinks]]
725api = "sink2"
726"#,
727 )
728 .unwrap();
729
730 let p2 = AnalysisProfile::parse(
731 r#"
732[[sinks]]
733api = "sink3"
734"#,
735 )
736 .unwrap();
737
738 let merged = AnalysisProfile::merge(vec![p1, p2]).unwrap();
739 assert_eq!(merged.sinks.len(), 3);
740 assert!(merged.is_sink("sink1").is_some());
741 assert!(merged.is_sink("sink2").is_some());
742 assert!(merged.is_sink("sink3").is_some());
743 }
744
745 #[test]
750 fn is_sink_exact_match_required() {
751 let toml = r#"
752[[sinks]]
753api = "chrome.tabs.executeScript"
754"#;
755 let profile = AnalysisProfile::parse(toml).unwrap();
756 assert!(profile.is_sink("chrome.tabs.executeScript").is_some());
757 assert!(profile.is_sink("chrome.tabs").is_none());
759 assert!(profile.is_sink("tabs.executeScript").is_none());
760 assert!(profile.is_sink("executeScript").is_none());
761 assert!(
762 profile
763 .is_sink("chrome.tabs.executeScript.details")
764 .is_none()
765 );
766 }
767
768 #[test]
769 fn is_source_exact_match_required() {
770 let toml = r#"
771[[sources]]
772api = "chrome.runtime.onMessage"
773taint_id = 1
774"#;
775 let profile = AnalysisProfile::parse(toml).unwrap();
776 assert!(profile.is_source("chrome.runtime.onMessage").is_some());
777 assert!(profile.is_source("chrome.runtime").is_none());
778 assert!(profile.is_source("runtime.onMessage").is_none());
779 assert!(profile.is_source("onMessage").is_none());
780 }
781
782 #[test]
783 fn is_sink_case_sensitive() {
784 let toml = r#"
785[[sinks]]
786api = "Eval"
787"#;
788 let profile = AnalysisProfile::parse(toml).unwrap();
789 assert!(profile.is_sink("Eval").is_some());
790 let result = profile.is_sink("eval");
793 let _ = result; }
795
796 #[test]
797 fn is_sink_empty_string() {
798 let toml = r#"
799[[sinks]]
800api = ""
801"#;
802 let profile = AnalysisProfile::parse(toml).unwrap();
803 assert!(profile.is_sink("").is_some());
804 assert!(profile.is_sink("something").is_none());
805 }
806
807 #[test]
812 fn kills_taint_multiple_categories() {
813 let toml = r#"
814[[sanitizers]]
815api = "sanitize"
816kills_taint = ["xss", "sqli", "commandi"]
817"#;
818 let profile = AnalysisProfile::parse(toml).unwrap();
819 assert!(profile.kills_taint("sanitize", "xss"));
820 assert!(profile.kills_taint("sanitize", "sqli"));
821 assert!(profile.kills_taint("sanitize", "commandi"));
822 assert!(!profile.kills_taint("sanitize", "path_traversal"));
823 }
824
825 #[test]
826 fn kills_taint_empty_categories() {
827 let toml = r#"
828[[sanitizers]]
829api = "sanitize"
830kills_taint = []
831"#;
832 let profile = AnalysisProfile::parse(toml).unwrap();
833 assert!(!profile.kills_taint("sanitize", "xss"));
834 }
835
836 #[test]
837 fn kills_taint_no_sanitizer_match() {
838 let toml = r#"
839[[sanitizers]]
840api = "DOMPurify.sanitize"
841kills_taint = ["xss"]
842"#;
843 let profile = AnalysisProfile::parse(toml).unwrap();
844 assert!(!profile.kills_taint("OtherSanitizer", "xss"));
845 }
846
847 #[test]
848 fn kills_taint_case_sensitive_category() {
849 let toml = r#"
850[[sanitizers]]
851api = "sanitize"
852kills_taint = ["XSS"]
853"#;
854 let profile = AnalysisProfile::parse(toml).unwrap();
855 assert!(profile.kills_taint("sanitize", "XSS"));
856 assert!(!profile.kills_taint("sanitize", "xss"));
858 }
859
860 #[test]
861 fn kills_taint_multiple_sanitizers() {
862 let toml = r#"
863[[sanitizers]]
864api = "sanitizer1"
865kills_taint = ["xss"]
866[[sanitizers]]
867api = "sanitizer2"
868kills_taint = ["sqli"]
869"#;
870 let profile = AnalysisProfile::parse(toml).unwrap();
871 assert!(profile.kills_taint("sanitizer1", "xss"));
872 assert!(profile.kills_taint("sanitizer2", "sqli"));
873 assert!(!profile.kills_taint("sanitizer1", "sqli"));
874 assert!(!profile.kills_taint("sanitizer2", "xss"));
875 }
876
877 #[test]
882 fn profile_with_unicode_api_names() {
883 let toml = r#"
884[profile]
885name = "unicode"
886[[sources]]
887api = "日本語.メッセージ"
888taint_id = 1
889[[sinks]]
890api = "🎉.celebrate"
891"#;
892 let profile = AnalysisProfile::parse(toml).unwrap();
893 assert!(profile.is_source("日本語.メッセージ").is_some());
894 assert!(profile.is_sink("🎉.celebrate").is_some());
895 }
896
897 #[test]
898 fn profile_with_unicode_in_description() {
899 let toml = r#"
900[profile]
901name = "unicode"
902description = "日本語の説明"
903[[sources]]
904api = "source"
905taint_id = 1
906description = "這是中文"
907"#;
908 let profile = AnalysisProfile::parse(toml).unwrap();
909 assert_eq!(profile.profile.description, "日本語の説明");
910 assert_eq!(profile.sources[0].description, "這是中文");
911 }
912
913 #[test]
914 fn profile_with_special_chars_in_api() {
915 let toml = r#"
916[[sources]]
917api = "api-name_with.special$chars"
918taint_id = 1
919"#;
920 let profile = AnalysisProfile::parse(toml).unwrap();
921 assert!(profile.is_source("api-name_with.special$chars").is_some());
922 }
923
924 #[test]
929 fn profile_empty_api_name() {
930 let toml = r#"
931[[sources]]
932api = ""
933taint_id = 1
934[[sinks]]
935api = ""
936"#;
937 let profile = AnalysisProfile::parse(toml).unwrap();
938 assert!(profile.is_source("").is_some());
939 assert!(profile.is_sink("").is_some());
940 }
941
942 #[test]
943 fn profile_empty_profile_name() {
944 let toml = r#"
945[profile]
946name = ""
947"#;
948 let profile = AnalysisProfile::parse(toml).unwrap();
949 assert_eq!(profile.profile.name, "");
950 }
951
952 #[test]
953 fn profile_empty_cwe() {
954 let toml = r#"
955[[sinks]]
956api = "sink1"
957cwe = ""
958"#;
959 let profile = AnalysisProfile::parse(toml).unwrap();
960 assert_eq!(profile.sinks[0].cwe, "");
961 }
962
963 #[test]
964 fn profile_empty_severity() {
965 let toml = r#"
966[[sinks]]
967api = "sink1"
968severity = ""
969"#;
970 let profile = AnalysisProfile::parse(toml).unwrap();
971 assert_eq!(profile.sinks[0].severity, "");
973 }
974
975 #[test]
980 fn rule_count_empty() {
981 let profile = AnalysisProfile::default();
982 assert_eq!(profile.rule_count(), 0);
983 }
984
985 #[test]
986 fn rule_count_only_sources() {
987 let toml = r#"
988[[sources]]
989api = "s1"
990taint_id = 1
991[[sources]]
992api = "s2"
993taint_id = 2
994"#;
995 let profile = AnalysisProfile::parse(toml).unwrap();
996 assert_eq!(profile.rule_count(), 2);
997 }
998
999 #[test]
1000 fn rule_count_only_sinks() {
1001 let toml = r#"
1002[[sinks]]
1003api = "sink1"
1004[[sinks]]
1005api = "sink2"
1006[[sinks]]
1007api = "sink3"
1008"#;
1009 let profile = AnalysisProfile::parse(toml).unwrap();
1010 assert_eq!(profile.rule_count(), 3);
1011 }
1012
1013 #[test]
1014 fn rule_count_only_transforms() {
1015 let toml = r#"
1016[[transforms]]
1017api = "t1"
1018[[transforms]]
1019api = "t2"
1020"#;
1021 let profile = AnalysisProfile::parse(toml).unwrap();
1022 assert_eq!(profile.rule_count(), 2);
1023 }
1024
1025 #[test]
1026 fn rule_count_only_sanitizers() {
1027 let toml = r#"
1028[[sanitizers]]
1029api = "s1"
1030[[sanitizers]]
1031api = "s2"
1032[[sanitizers]]
1033api = "s3"
1034[[sanitizers]]
1035api = "s4"
1036"#;
1037 let profile = AnalysisProfile::parse(toml).unwrap();
1038 assert_eq!(profile.rule_count(), 4);
1039 }
1040
1041 #[test]
1042 fn rule_count_mixed() {
1043 let toml = r#"
1044[[sources]]
1045api = "s1"
1046taint_id = 1
1047[[sinks]]
1048api = "sink1"
1049[[transforms]]
1050api = "t1"
1051[[sanitizers]]
1052api = "san1"
1053"#;
1054 let profile = AnalysisProfile::parse(toml).unwrap();
1055 assert_eq!(profile.rule_count(), 4);
1056 }
1057
1058 #[test]
1059 fn rule_count_large_profile() {
1060 let sources: String = (1..=100)
1061 .map(|i| format!("[[sources]]\napi = \"source{}\"\ntaint_id = {}\n", i, i))
1062 .collect();
1063 let profile = AnalysisProfile::parse(&sources).unwrap();
1064 assert_eq!(profile.rule_count(), 100);
1065 }
1066
1067 #[test]
1072 fn default_severity_is_high() {
1073 let toml = r#"
1074[[sinks]]
1075api = "sink1"
1076"#;
1077 let profile = AnalysisProfile::parse(toml).unwrap();
1078 assert_eq!(profile.sinks[0].severity, "high");
1079 }
1080
1081 #[test]
1082 fn default_extract_is_args0() {
1083 let toml = r#"
1084[[sources]]
1085api = "source1"
1086taint_id = 1
1087"#;
1088 let profile = AnalysisProfile::parse(toml).unwrap();
1089 assert_eq!(profile.sources[0].extract, "args[0]");
1090 }
1091
1092 #[test]
1093 fn default_propagates_taint_is_true() {
1094 let toml = r#"
1095[[transforms]]
1096api = "transform1"
1097"#;
1098 let profile = AnalysisProfile::parse(toml).unwrap();
1099 assert!(profile.transforms[0].propagates_taint);
1100 }
1101
1102 #[test]
1103 fn default_dangerous_arg_is_0() {
1104 let toml = r#"
1105[[sinks]]
1106api = "sink1"
1107"#;
1108 let profile = AnalysisProfile::parse(toml).unwrap();
1109 assert_eq!(profile.sinks[0].dangerous_arg, 0);
1110 }
1111
1112 #[test]
1113 fn default_kills_taint_is_empty() {
1114 let toml = r#"
1115[[sanitizers]]
1116api = "sanitizer1"
1117"#;
1118 let profile = AnalysisProfile::parse(toml).unwrap();
1119 assert!(profile.sanitizers[0].kills_taint.is_empty());
1120 }
1121
1122 #[test]
1123 fn default_profile_fields() {
1124 let toml = r#"
1125[profile]
1126name = "test"
1127"#;
1128 let profile = AnalysisProfile::parse(toml).unwrap();
1129 assert_eq!(profile.profile.description, "");
1130 assert_eq!(profile.profile.version, "");
1131 assert_eq!(profile.profile.author, "");
1132 }
1133
1134 #[test]
1135 fn explicit_values_override_defaults() {
1136 let toml = r#"
1137[[sinks]]
1138api = "sink1"
1139severity = "critical"
1140dangerous_arg = 2
1141[[sources]]
1142api = "source1"
1143taint_id = 1
1144extract = "return"
1145[[transforms]]
1146api = "transform1"
1147propagates_taint = false
1148"#;
1149 let profile = AnalysisProfile::parse(toml).unwrap();
1150 assert_eq!(profile.sinks[0].severity, "critical");
1151 assert_eq!(profile.sinks[0].dangerous_arg, 2);
1152 assert_eq!(profile.sources[0].extract, "return");
1153 assert!(!profile.transforms[0].propagates_taint);
1154 }
1155
1156 #[test]
1161 fn source_with_all_fields() {
1162 let toml = r#"
1163[[sources]]
1164api = "chrome.runtime.onMessage"
1165taint_id = 42
1166extract = "event.data"
1167description = "Message from runtime"
1168"#;
1169 let profile = AnalysisProfile::parse(toml).unwrap();
1170 let source = &profile.sources[0];
1171 assert_eq!(source.api, "chrome.runtime.onMessage");
1172 assert_eq!(source.taint_id, 42);
1173 assert_eq!(source.extract, "event.data");
1174 assert_eq!(source.description, "Message from runtime");
1175 }
1176
1177 #[test]
1178 fn source_large_taint_id() {
1179 let toml = r#"
1180[[sources]]
1181api = "source1"
1182taint_id = 4294967295
1183"#;
1184 let profile = AnalysisProfile::parse(toml).unwrap();
1185 assert_eq!(profile.sources[0].taint_id, 4294967295);
1186 }
1187
1188 #[test]
1189 fn source_taint_id_zero() {
1190 let toml = r#"
1191[[sources]]
1192api = "source1"
1193taint_id = 0
1194"#;
1195 let profile = AnalysisProfile::parse(toml).unwrap();
1196 assert_eq!(profile.sources[0].taint_id, 0);
1197 }
1198
1199 #[test]
1204 fn sink_with_all_fields() {
1205 let toml = r#"
1206[[sinks]]
1207api = "eval"
1208dangerous_arg = 0
1209severity = "critical"
1210cwe = "CWE-95"
1211description = "Code execution"
1212"#;
1213 let profile = AnalysisProfile::parse(toml).unwrap();
1214 let sink = profile.is_sink("eval").unwrap();
1215 assert_eq!(sink.dangerous_arg, 0);
1216 assert_eq!(sink.severity, "critical");
1217 assert_eq!(sink.cwe, "CWE-95");
1218 assert_eq!(sink.description, "Code execution");
1219 }
1220
1221 #[test]
1222 fn sink_large_dangerous_arg() {
1223 let toml = r#"
1224[[sinks]]
1225api = "sink1"
1226dangerous_arg = 999
1227"#;
1228 let profile = AnalysisProfile::parse(toml).unwrap();
1229 assert_eq!(profile.sinks[0].dangerous_arg, 999);
1230 }
1231
1232 #[test]
1237 fn transform_propagates_true() {
1238 let toml = r#"
1239[[transforms]]
1240api = "JSON.parse"
1241propagates_taint = true
1242"#;
1243 let profile = AnalysisProfile::parse(toml).unwrap();
1244 assert!(profile.transforms[0].propagates_taint);
1245 }
1246
1247 #[test]
1248 fn transform_propagates_false() {
1249 let toml = r#"
1250[[transforms]]
1251api = "toString"
1252propagates_taint = false
1253"#;
1254 let profile = AnalysisProfile::parse(toml).unwrap();
1255 assert!(!profile.transforms[0].propagates_taint);
1256 }
1257
1258 #[test]
1263 fn sanitizer_single_category() {
1264 let toml = r#"
1265[[sanitizers]]
1266api = "sanitize"
1267kills_taint = ["xss"]
1268"#;
1269 let profile = AnalysisProfile::parse(toml).unwrap();
1270 assert_eq!(profile.sanitizers[0].kills_taint.len(), 1);
1271 }
1272
1273 #[test]
1274 fn sanitizer_many_categories() {
1275 let toml = r#"
1276[[sanitizers]]
1277api = "superSanitizer"
1278kills_taint = ["xss", "sqli", "commandi", "path_traversal", "ssrf", "xxe", "ldap_injection"]
1279"#;
1280 let profile = AnalysisProfile::parse(toml).unwrap();
1281 assert_eq!(profile.sanitizers[0].kills_taint.len(), 7);
1282 }
1283}