1use serde_json::{Map, Value};
14
15use crate::constraint::{parse_constraints, Constraint};
16
17#[derive(Debug, thiserror::Error)]
18#[error("{0}")]
19pub struct PolicyError(pub String);
20
21#[derive(Debug, Clone)]
23pub struct IgnorePackageRule {
24 pub package_name: String,
25 pub constraint: Constraint,
26 pub reason: Option<String>,
27 pub on_block: bool,
28 pub on_audit: bool,
29}
30
31#[derive(Debug, Clone)]
33pub struct IgnoreIdRule {
34 pub id: String,
35 pub reason: Option<String>,
36 pub on_block: bool,
37 pub on_audit: bool,
38}
39
40#[derive(Debug, Clone)]
42pub struct IgnoreSeverityRule {
43 pub severity: String,
44 pub reason: Option<String>,
45 pub on_block: bool,
46 pub on_audit: bool,
47}
48
49pub type IgnoreMap = Vec<(String, Vec<IgnorePackageRule>)>;
51
52#[derive(Debug, Clone)]
53pub struct AdvisoriesPolicy {
54 pub block: bool,
55 pub ignore: IgnoreMap,
56 pub ignore_id: Vec<IgnoreIdRule>,
57 pub ignore_severity: Vec<IgnoreSeverityRule>,
58}
59
60#[derive(Debug, Clone)]
61pub struct MalwarePolicy {
62 pub block: bool,
63 pub block_scope: String,
65 pub ignore: IgnoreMap,
66 pub ignore_source: Vec<String>,
67}
68
69#[derive(Debug, Clone)]
70pub struct AbandonedPolicy {
71 pub block: bool,
72 pub ignore: IgnoreMap,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct IgnoreUnreachable {
79 pub audit: bool,
80 pub install: bool,
81 pub update: bool,
82}
83
84impl IgnoreUnreachable {
85 pub fn for_block_scope(&self, scope: &str) -> bool {
86 if scope == "install" {
87 self.install
88 } else {
89 self.update
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
95pub struct PolicyConfig {
96 pub enabled: bool,
98 pub advisories: AdvisoriesPolicy,
99 pub malware: MalwarePolicy,
100 pub abandoned: AbandonedPolicy,
101 pub custom_lists: Vec<String>,
103 pub ignore_unreachable: IgnoreUnreachable,
104}
105
106const NON_LIST_KEYS: &[&str] = &["ignore-unreachable"];
107const BUILTIN_LIST_NAMES: &[&str] = &["advisories", "malware", "abandoned"];
108
109pub fn bool_env(name: &str) -> Result<Option<bool>, PolicyError> {
112 match std::env::var(name) {
113 Ok(v) if !v.is_empty() => match v.as_str() {
114 "1" | "true" | "on" => Ok(Some(true)),
115 "0" | "false" | "off" => Ok(Some(false)),
116 other => Err(PolicyError(format!(
117 "Invalid value for {name}: {other}. Expected 0, 1, false, true, off, or on."
118 ))),
119 },
120 _ => Ok(None),
121 }
122}
123
124#[derive(Debug, Clone)]
127pub struct RawPolicyConfig {
128 pub policy: Value,
130 pub audit: Value,
131}
132
133impl Default for RawPolicyConfig {
134 fn default() -> Self {
135 RawPolicyConfig {
136 policy: Value::Bool(true),
137 audit: serde_json::json!({"ignore": [], "abandoned": "fail"}),
138 }
139 }
140}
141
142impl RawPolicyConfig {
143 pub fn merge(&mut self, config: &Map<String, Value>) {
145 if let Some(val) = config.get("audit") {
146 let current_ignores = self
147 .audit
148 .get("ignore")
149 .cloned()
150 .unwrap_or(Value::Array(Vec::new()));
151 let mut merged = php_array_merge(&self.audit, val);
152 let incoming = val
153 .get("ignore")
154 .cloned()
155 .unwrap_or(Value::Array(Vec::new()));
156 if let Value::Object(m) = &mut merged {
157 m.insert(
158 "ignore".into(),
159 php_array_merge(¤t_ignores, &incoming),
160 );
161 }
162 self.audit = merged;
163 }
164 if let Some(val) = config.get("policy") {
165 let val = if val == &Value::Bool(true) {
166 Value::Object(Map::new())
167 } else {
168 val.clone()
169 };
170 match val {
171 Value::Bool(false) => self.policy = Value::Bool(false),
172 Value::Object(lists) => {
173 let mut current = match &self.policy {
174 Value::Object(m) => m.clone(),
175 _ => Map::new(),
176 };
177 for (list_name, list_config) in lists {
178 if NON_LIST_KEYS.contains(&list_name.as_str()) {
179 current.insert(list_name, list_config);
180 continue;
181 }
182 let list_config = if list_config == Value::Bool(true) {
183 Value::Object(Map::new())
184 } else {
185 list_config
186 };
187 let existing = match current.get(&list_name) {
188 Some(Value::Bool(true)) => Some(Value::Object(Map::new())),
189 Some(v) => Some(v.clone()),
190 None => None,
191 };
192 let merged = match (&existing, &list_config) {
193 (_, Value::Bool(false)) => Value::Bool(false),
194 (None | Some(Value::Bool(false)), _) => list_config.clone(),
195 (Some(Value::Object(e)), Value::Object(i)) => {
196 let mut m = php_array_merge(
197 &Value::Object(e.clone()),
198 &Value::Object(i.clone()),
199 );
200 for inner in
201 ["ignore", "ignore-id", "ignore-severity", "ignore-source"]
202 {
203 if let (Some(ei), Some(ii)) = (e.get(inner), i.get(inner)) {
204 if is_php_array(ei) && is_php_array(ii) {
205 if let Value::Object(mm) = &mut m {
206 mm.insert(inner.into(), php_array_merge(ei, ii));
207 }
208 }
209 }
210 }
211 m
212 }
213 _ => list_config.clone(),
214 };
215 current.insert(list_name, merged);
216 }
217 self.policy = Value::Object(current);
218 }
219 _ => {}
220 }
221 }
222 }
223
224 pub fn effective_policy(&self) -> Result<Value, PolicyError> {
227 Ok(match bool_env("COMPOSER_POLICY")? {
228 Some(false) => Value::Bool(false),
229 Some(true) if self.policy == Value::Bool(false) => Value::Bool(true),
230 _ => self.policy.clone(),
231 })
232 }
233}
234
235fn is_php_array(v: &Value) -> bool {
236 matches!(v, Value::Array(_) | Value::Object(_))
237}
238
239fn php_array_merge(a: &Value, b: &Value) -> Value {
243 fn entries(v: &Value) -> Vec<(Option<String>, Value)> {
244 match v {
245 Value::Array(items) => items.iter().map(|x| (None, x.clone())).collect(),
246 Value::Object(m) => m
247 .iter()
248 .map(|(k, x)| {
249 let is_int = k == "0"
251 || (k.starts_with(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
252 && k.bytes().all(|c| c.is_ascii_digit()));
253 (if is_int { None } else { Some(k.clone()) }, x.clone())
254 })
255 .collect(),
256 _ => Vec::new(),
257 }
258 }
259 let mut out: Vec<(Option<String>, Value)> = Vec::new();
260 for (k, v) in entries(a).into_iter().chain(entries(b)) {
261 match &k {
262 Some(key) => match out.iter_mut().find(|(ok, _)| ok.as_deref() == Some(key)) {
263 Some(slot) => slot.1 = v,
264 None => out.push((k, v)),
265 },
266 None => out.push((None, v)),
267 }
268 }
269 if out.iter().all(|(k, _)| k.is_none()) {
270 return Value::Array(out.into_iter().map(|(_, v)| v).collect());
271 }
272 let mut m = Map::new();
273 let mut idx = 0;
274 for (k, v) in out {
275 match k {
276 Some(key) => {
277 m.insert(key, v);
278 }
279 None => {
280 m.insert(idx.to_string(), v);
281 idx += 1;
282 }
283 }
284 }
285 Value::Object(m)
286}
287
288fn php_entries(v: &Value) -> Vec<(Option<String>, Value)> {
290 match v {
291 Value::Array(items) => items.iter().map(|x| (None, x.clone())).collect(),
292 Value::Object(m) => m
293 .iter()
294 .map(|(k, x)| {
295 let is_int = k == "0"
296 || (k.starts_with(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
297 && k.bytes().all(|c| c.is_ascii_digit()));
298 (if is_int { None } else { Some(k.clone()) }, x.clone())
299 })
300 .collect(),
301 _ => Vec::new(),
302 }
303}
304
305fn php_truthy(v: &Value) -> bool {
306 match v {
307 Value::Null => false,
308 Value::Bool(b) => *b,
309 Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
310 Value::String(s) => !(s.is_empty() || s == "0"),
311 Value::Array(a) => !a.is_empty(),
312 Value::Object(m) => !m.is_empty(),
313 }
314}
315
316fn opt_str(v: Option<&Value>) -> Option<String> {
317 v.and_then(Value::as_str).map(str::to_owned)
318}
319
320fn bool_or(m: &Map<String, Value>, key: &str, default: bool) -> bool {
322 m.get(key).map(php_truthy).unwrap_or(default)
323}
324
325fn legacy_single(
327 key: &Option<String>,
328 value: &Value,
329) -> Result<(Option<String>, bool, bool), PolicyError> {
330 let mut reason = None;
331 let mut on_block = true;
332 let mut on_audit = true;
333 if key.is_none() && value.is_string() {
334 } else if let Some(s) = value.as_str() {
336 reason = Some(s.to_owned());
337 } else if let Value::Object(v) = value {
338 let apply = v.get("apply").and_then(Value::as_str).unwrap_or("all");
339 reason = opt_str(v.get("reason"));
340 if !["audit", "block", "all"].contains(&apply) {
341 return Err(PolicyError(format!(
342 "Invalid 'apply' value for '{}': {apply}. Expected 'audit', 'block', or 'all'.",
343 key.clone().unwrap_or_default()
344 )));
345 }
346 on_block = apply == "block" || apply == "all";
347 on_audit = apply == "audit" || apply == "all";
348 }
349 Ok((reason, on_block, on_audit))
350}
351
352fn parse_legacy_audit_ignore(
355 config: &Value,
356) -> Result<(IgnoreMap, Vec<IgnoreIdRule>), PolicyError> {
357 let mut packages: IgnoreMap = Vec::new();
358 let mut ids = Vec::new();
359 for (key, value) in php_entries(config) {
360 let id = match &key {
361 Some(k) => k.clone(),
362 None => php_to_string(&value),
363 };
364 let (reason, on_block, on_audit) = legacy_single(&key, &value)?;
365 if id.contains('/') {
366 push_rule(
367 &mut packages,
368 IgnorePackageRule {
369 package_name: id,
370 constraint: Constraint::MatchAll,
371 reason,
372 on_block,
373 on_audit,
374 },
375 );
376 } else {
377 ids.push(IgnoreIdRule {
378 id,
379 reason,
380 on_block,
381 on_audit,
382 });
383 }
384 }
385 Ok((packages, ids))
386}
387
388fn parse_legacy_ignore_with_apply(config: &Value) -> Result<IgnoreMap, PolicyError> {
390 let mut out: IgnoreMap = Vec::new();
391 for (key, value) in php_entries(config) {
392 let name = match &key {
393 Some(k) => k.clone(),
394 None => php_to_string(&value),
395 };
396 let (reason, on_block, on_audit) = legacy_single(&key, &value)?;
397 out.retain(|(n, _)| *n != name);
399 out.push((
400 name.clone(),
401 vec![IgnorePackageRule {
402 package_name: name,
403 constraint: Constraint::MatchAll,
404 reason,
405 on_block,
406 on_audit,
407 }],
408 ));
409 }
410 Ok(out)
411}
412
413fn php_to_string(v: &Value) -> String {
414 match v {
415 Value::String(s) => s.clone(),
416 Value::Number(n) => n.to_string(),
417 Value::Bool(true) => "1".into(),
418 _ => String::new(),
419 }
420}
421
422fn push_rule(map: &mut IgnoreMap, rule: IgnorePackageRule) {
423 match map.iter_mut().find(|(n, _)| *n == rule.package_name) {
424 Some((_, rules)) => rules.push(rule),
425 None => map.push((rule.package_name.clone(), vec![rule])),
426 }
427}
428
429fn parse_ignore_map(config: &Value) -> Result<IgnoreMap, PolicyError> {
431 let mut rules: IgnoreMap = Vec::new();
432 let from_rule_object =
433 |name: &str, v: &Map<String, Value>| -> Result<IgnorePackageRule, PolicyError> {
434 let constraint = match v.get("constraint") {
435 Some(c) => {
436 parse_constraints(&php_to_string(c))
437 .map_err(|e| PolicyError(e.to_string()))?
438 .constraint
439 }
440 None => Constraint::MatchAll,
441 };
442 Ok(IgnorePackageRule {
443 package_name: name.to_owned(),
444 constraint,
445 reason: opt_str(v.get("reason")),
446 on_block: v.get("on-block").map(php_truthy).unwrap_or(true),
447 on_audit: v.get("on-audit").map(php_truthy).unwrap_or(true),
448 })
449 };
450 for (key, value) in php_entries(config) {
451 match (&key, &value) {
452 (Some(k), Value::Null) => push_rule(
453 &mut rules,
454 IgnorePackageRule {
455 package_name: k.clone(),
456 constraint: Constraint::MatchAll,
457 reason: None,
458 on_block: true,
459 on_audit: true,
460 },
461 ),
462 (Some(k), Value::String(s)) => push_rule(
463 &mut rules,
464 IgnorePackageRule {
465 package_name: k.clone(),
466 constraint: Constraint::MatchAll,
467 reason: Some(s.clone()),
468 on_block: true,
469 on_audit: true,
470 },
471 ),
472 (None, Value::String(s)) => push_rule(
473 &mut rules,
474 IgnorePackageRule {
475 package_name: s.clone(),
476 constraint: Constraint::MatchAll,
477 reason: None,
478 on_block: true,
479 on_audit: true,
480 },
481 ),
482 (Some(k), Value::Array(list)) => {
483 if list.is_empty() {
485 let r = from_rule_object(k, &Map::new())?;
486 push_rule(&mut rules, r);
487 }
488 for rule in list {
489 let Value::Object(o) = rule else {
490 return Err(PolicyError(format!(
491 "Invalid ignore rule for \"{k}\": expected an object, got {}.",
492 php_type(rule)
493 )));
494 };
495 let r = from_rule_object(k, o)?;
496 push_rule(&mut rules, r);
497 }
498 }
499 (Some(k), Value::Object(o)) if o.contains_key("0") => {
500 for rule in o.values() {
501 let Value::Object(ro) = rule else {
502 return Err(PolicyError(format!(
503 "Invalid ignore rule for \"{k}\": expected an object, got {}.",
504 php_type(rule)
505 )));
506 };
507 let r = from_rule_object(k, ro)?;
508 push_rule(&mut rules, r);
509 }
510 }
511 (Some(k), Value::Object(o)) => {
512 let r = from_rule_object(k, o)?;
513 push_rule(&mut rules, r);
514 }
515 (k, v) => {
516 return Err(PolicyError(format!(
517 "Invalid ignore entry at key \"{}\": value of type {} is not a supported shape. Expected null, a reason string, a rule object, or a list of rule objects.",
518 k.clone().unwrap_or_default(),
519 php_type(v)
520 )))
521 }
522 }
523 }
524 Ok(rules)
525}
526
527fn php_type(v: &Value) -> &'static str {
528 match v {
529 Value::Null => "null",
530 Value::Bool(_) => "bool",
531 Value::Number(n) if n.is_f64() => "float",
532 Value::Number(_) => "int",
533 Value::String(_) => "string",
534 Value::Array(_) | Value::Object(_) => "array",
535 }
536}
537
538fn parse_ignore_id_map(config: &Value) -> Result<Vec<IgnoreIdRule>, PolicyError> {
540 let mut rules: Vec<IgnoreIdRule> = Vec::new();
541 let mut set = |rule: IgnoreIdRule| match rules.iter_mut().find(|r| r.id == rule.id) {
542 Some(slot) => *slot = rule,
543 None => rules.push(rule),
544 };
545 for (key, value) in php_entries(config) {
546 match (&key, &value) {
547 (None, Value::String(s)) => set(IgnoreIdRule {
548 id: s.clone(),
549 reason: None,
550 on_block: true,
551 on_audit: true,
552 }),
553 (None, other) => {
554 return Err(PolicyError(format!(
555 "Invalid ignore-id entry: expected an advisory ID string, got {}.",
556 php_type(other)
557 )))
558 }
559 (Some(k), Value::Null) => set(IgnoreIdRule {
560 id: k.clone(),
561 reason: None,
562 on_block: true,
563 on_audit: true,
564 }),
565 (Some(k), Value::String(s)) => set(IgnoreIdRule {
566 id: k.clone(),
567 reason: Some(s.clone()),
568 on_block: true,
569 on_audit: true,
570 }),
571 (Some(k), Value::Object(o)) => set(IgnoreIdRule {
572 id: k.clone(),
573 reason: opt_str(o.get("reason")),
574 on_block: o.get("on-block").map(php_truthy).unwrap_or(true),
575 on_audit: o.get("on-audit").map(php_truthy).unwrap_or(true),
576 }),
577 (Some(k), other) => {
578 return Err(PolicyError(format!(
579 "Invalid ignore-id entry for \"{k}\": value of type {} is not a supported shape. Expected null, a reason string, or a rule object.",
580 php_type(other)
581 )))
582 }
583 }
584 }
585 Ok(rules)
586}
587
588fn parse_legacy_ignore_severity(config: &Value) -> Result<Vec<IgnoreSeverityRule>, PolicyError> {
591 let mut rules: Vec<IgnoreSeverityRule> = Vec::new();
592 for (key, value) in php_entries(config) {
593 let severity = match &key {
594 Some(k) => k.clone(),
595 None => php_to_string(&value),
596 };
597 let (reason, on_block, on_audit) = legacy_single(&key, &value)?;
598 rules.retain(|r| r.severity != severity);
599 rules.push(IgnoreSeverityRule {
600 severity,
601 reason,
602 on_block,
603 on_audit,
604 });
605 }
606 Ok(rules)
607}
608
609fn parse_ignore_severity(config: &Value) -> Result<Vec<IgnoreSeverityRule>, PolicyError> {
612 let mut rules: Vec<IgnoreSeverityRule> = Vec::new();
613 for (key, value) in php_entries(config) {
614 let (severity, reason, on_block, on_audit) = match (&key, &value) {
615 (None, Value::String(s)) => (s.clone(), None, true, true),
616 (Some(k), Value::Null) => (k.clone(), None, true, true),
617 (Some(k), Value::String(s)) => (k.clone(), Some(s.clone()), true, true),
618 (Some(k), Value::Object(o)) => (
619 k.clone(),
620 opt_str(o.get("reason")),
621 o.get("on-block").map(php_truthy).unwrap_or(true),
622 o.get("on-audit").map(php_truthy).unwrap_or(true),
623 ),
624 (k, other) => {
625 return Err(PolicyError(format!(
626 "Invalid ignore-severity entry \"{}\": value of type {} is not a supported shape.",
627 k.clone().unwrap_or_default(),
628 php_type(other)
629 )))
630 }
631 };
632 rules.retain(|r| r.severity != severity);
633 rules.push(IgnoreSeverityRule {
634 severity,
635 reason,
636 on_block,
637 on_audit,
638 });
639 }
640 Ok(rules)
641}
642
643impl PolicyConfig {
644 pub fn from_raw(raw: &RawPolicyConfig) -> Result<PolicyConfig, PolicyError> {
646 let policy = raw.effective_policy()?;
647 if policy == Value::Bool(false) {
648 return Ok(PolicyConfig {
649 enabled: false,
650 advisories: AdvisoriesPolicy {
651 block: false,
652 ignore: Vec::new(),
653 ignore_id: Vec::new(),
654 ignore_severity: Vec::new(),
655 },
656 malware: MalwarePolicy {
657 block: false,
658 block_scope: "all".into(),
659 ignore: Vec::new(),
660 ignore_source: Vec::new(),
661 },
662 abandoned: AbandonedPolicy {
663 block: false,
664 ignore: Vec::new(),
665 },
666 custom_lists: Vec::new(),
667 ignore_unreachable: IgnoreUnreachable {
668 audit: true,
669 install: true,
670 update: true,
671 },
672 });
673 }
674 let policy_cfg = match &policy {
675 Value::Object(m) => m.clone(),
676 _ => Map::new(),
677 };
678 let audit_cfg = match &raw.audit {
679 Value::Object(m) => m.clone(),
680 _ => Map::new(),
681 };
682 let empty_audit = audit_cfg.is_empty();
683 let empty = Value::Array(Vec::new());
684
685 let mut advisories = if !policy_cfg.contains_key("advisories") && !empty_audit {
687 let (ignore, ignore_id) =
688 parse_legacy_audit_ignore(audit_cfg.get("ignore").unwrap_or(&empty))?;
689 AdvisoriesPolicy {
690 block: bool_or(&audit_cfg, "block-insecure", true),
691 ignore,
692 ignore_id,
693 ignore_severity: parse_legacy_ignore_severity(
694 audit_cfg.get("ignore-severity").unwrap_or(&empty),
695 )?,
696 }
697 } else {
698 match policy_cfg.get("advisories") {
699 Some(Value::Bool(false)) => AdvisoriesPolicy {
700 block: false,
701 ignore: Vec::new(),
702 ignore_id: Vec::new(),
703 ignore_severity: Vec::new(),
704 },
705 other => {
706 let cfg = match other {
707 Some(Value::Object(m)) => m.clone(),
708 _ => Map::new(),
709 };
710 AdvisoriesPolicy {
711 block: bool_or(&cfg, "block", true),
712 ignore: parse_ignore_map(cfg.get("ignore").unwrap_or(&empty))?,
713 ignore_id: parse_ignore_id_map(cfg.get("ignore-id").unwrap_or(&empty))?,
714 ignore_severity: parse_ignore_severity(
715 cfg.get("ignore-severity").unwrap_or(&empty),
716 )?,
717 }
718 }
719 }
720 };
721 let mut malware = match policy_cfg.get("malware") {
723 Some(Value::Bool(false)) => MalwarePolicy {
724 block: false,
725 block_scope: "all".into(),
726 ignore: Vec::new(),
727 ignore_source: Vec::new(),
728 },
729 other => {
730 let cfg = match other {
731 Some(Value::Object(m)) => m.clone(),
732 _ => Map::new(),
733 };
734 MalwarePolicy {
735 block: bool_or(&cfg, "block", true),
736 block_scope: opt_str(cfg.get("block-scope")).unwrap_or_else(|| "all".into()),
737 ignore: parse_ignore_map(cfg.get("ignore").unwrap_or(&empty))?,
738 ignore_source: cfg
739 .get("ignore-source")
740 .map(|v| {
741 php_entries(v)
742 .into_iter()
743 .map(|(_, x)| php_to_string(&x))
744 .collect()
745 })
746 .unwrap_or_default(),
747 }
748 }
749 };
750 let mut abandoned = if !policy_cfg.contains_key("abandoned") && !empty_audit {
752 AbandonedPolicy {
753 block: bool_or(&audit_cfg, "block-abandoned", false),
754 ignore: parse_legacy_ignore_with_apply(
755 audit_cfg.get("ignore-abandoned").unwrap_or(&empty),
756 )?,
757 }
758 } else {
759 match policy_cfg.get("abandoned") {
760 Some(Value::Bool(false)) => AbandonedPolicy {
761 block: false,
762 ignore: Vec::new(),
763 },
764 other => {
765 let cfg = match other {
766 Some(Value::Object(m)) => m.clone(),
767 _ => Map::new(),
768 };
769 AbandonedPolicy {
770 block: bool_or(&cfg, "block", false),
771 ignore: parse_ignore_map(cfg.get("ignore").unwrap_or(&empty))?,
772 }
773 }
774 }
775 };
776 let custom_lists: Vec<String> = policy_cfg
777 .keys()
778 .filter(|k| {
779 !BUILTIN_LIST_NAMES.contains(&k.as_str()) && !NON_LIST_KEYS.contains(&k.as_str())
780 })
781 .cloned()
782 .collect();
783 let ignore_unreachable = if let Some(v) = policy_cfg.get("ignore-unreachable") {
784 match v {
785 Value::Array(list) => {
786 let has = |s: &str| list.iter().any(|x| x.as_str() == Some(s));
787 IgnoreUnreachable {
788 audit: has("audit"),
789 install: has("install"),
790 update: has("update"),
791 }
792 }
793 other if php_truthy(other) => IgnoreUnreachable {
794 audit: true,
795 install: true,
796 update: true,
797 },
798 _ => IgnoreUnreachable {
799 audit: false,
800 install: false,
801 update: false,
802 },
803 }
804 } else if audit_cfg.get("ignore-unreachable").is_some_and(php_truthy) {
805 IgnoreUnreachable {
806 audit: true,
807 install: false,
808 update: false,
809 }
810 } else {
811 IgnoreUnreachable {
812 audit: false,
813 install: true,
814 update: true,
815 }
816 };
817 if let Ok(v) = std::env::var("COMPOSER_AUDIT_ABANDONED") {
820 if !["ignore", "report", "fail"].contains(&v.as_str()) {
821 return Err(PolicyError(format!(
822 "Invalid value for COMPOSER_AUDIT_ABANDONED: {v}. Expected one of ignore, report, fail."
823 )));
824 }
825 }
826 if let Some(b) = bool_env("COMPOSER_POLICY_ADVISORIES_BLOCK")? {
827 advisories.block = b;
828 }
829 if let Some(b) = bool_env("COMPOSER_POLICY_MALWARE_BLOCK")? {
830 malware.block = b;
831 }
832 let abandoned_env = match bool_env("COMPOSER_POLICY_ABANDONED_BLOCK")? {
833 Some(b) => Some(b),
834 None => bool_env("COMPOSER_SECURITY_BLOCKING_ABANDONED")?,
835 };
836 if let Some(b) = abandoned_env {
837 abandoned.block = b;
838 }
839 Ok(PolicyConfig {
840 enabled: true,
841 advisories,
842 malware,
843 abandoned,
844 custom_lists,
845 ignore_unreachable,
846 })
847 }
848
849 pub fn apply_no_blocking(&mut self, option: bool) -> Result<(), PolicyError> {
853 let no_blocking = option
854 || bool_env("COMPOSER_NO_BLOCKING")?.unwrap_or(false)
855 || bool_env("COMPOSER_NO_SECURITY_BLOCKING")?.unwrap_or(false);
856 if no_blocking {
857 self.advisories.block = false;
858 self.malware.block = false;
859 self.abandoned.block = false;
860 }
861 Ok(())
862 }
863
864 pub fn malware_blocks(&self, scope: &str) -> bool {
866 if !self.malware.block {
867 return false;
868 }
869 match self.malware.block_scope.as_str() {
870 "all" => true,
871 s => s == scope,
872 }
873 }
874}
875
876pub fn advisory_ignore_list_for_block(p: &AdvisoriesPolicy) -> Vec<(String, Option<String>)> {
879 let mut out: Vec<(String, Option<String>)> = Vec::new();
880 let mut set = |key: String, reason: Option<String>| {
881 match out.iter_mut().find(|(k, _)| *k == key) {
882 Some(slot) => {
885 if reason.is_some() {
886 slot.1 = reason;
887 }
888 }
889 None => out.push((key, reason)),
890 }
891 };
892 for r in &p.ignore_id {
893 if r.on_block {
894 set(r.id.clone(), r.reason.clone());
895 }
896 }
897 for (name, rules) in &p.ignore {
898 for r in rules {
899 if r.on_block {
900 set(name.clone(), r.reason.clone());
901 }
902 }
903 }
904 out
905}
906
907pub fn advisory_ignore_severity_for_block(p: &AdvisoriesPolicy) -> Vec<(String, Option<String>)> {
909 p.ignore_severity
910 .iter()
911 .filter(|r| r.on_block)
912 .map(|r| (r.severity.clone(), r.reason.clone()))
913 .collect()
914}
915
916pub fn flat_ignore_for_block(map: &IgnoreMap) -> Vec<(String, Option<String>)> {
919 let mut out: Vec<(String, Option<String>)> = Vec::new();
920 for (name, rules) in map {
921 for r in rules {
922 if r.on_block {
923 match out.iter_mut().find(|(k, _)| k == name) {
924 Some(slot) => {
925 if r.reason.is_some() {
926 slot.1 = r.reason.clone();
927 }
928 }
929 None => out.push((name.clone(), r.reason.clone())),
930 }
931 }
932 }
933 }
934 out
935}
936
937#[cfg(test)]
938mod tests {
939 use super::*;
940
941 #[test]
942 fn defaults_block_advisories_and_malware_only() {
943 let p = PolicyConfig::from_raw(&RawPolicyConfig::default()).expect("policy");
944 assert!(p.enabled);
945 assert!(p.advisories.block);
946 assert!(p.malware.block);
947 assert!(!p.abandoned.block);
948 assert_eq!(
949 p.ignore_unreachable,
950 IgnoreUnreachable {
951 audit: false,
952 install: true,
953 update: true
954 }
955 );
956 }
957
958 #[test]
959 fn legacy_audit_config_is_the_default_path() {
960 let mut raw = RawPolicyConfig::default();
961 let cfg: Map<String, Value> = serde_json::from_str(
962 r#"{"audit": {"ignore": ["CVE-2024-1", "acme/lib"], "block-insecure": false, "block-abandoned": true, "ignore-abandoned": {"old/pkg": "meh"}}}"#,
963 )
964 .expect("json");
965 raw.merge(&cfg);
966 let p = PolicyConfig::from_raw(&raw).expect("policy");
967 assert!(!p.advisories.block);
968 assert_eq!(p.advisories.ignore_id[0].id, "CVE-2024-1");
969 assert_eq!(p.advisories.ignore[0].0, "acme/lib");
970 assert!(p.abandoned.block);
971 assert_eq!(p.abandoned.ignore[0].1[0].reason.as_deref(), Some("meh"));
972 }
973
974 #[test]
975 fn policy_merge_global_then_project() {
976 let mut raw = RawPolicyConfig::default();
977 let global: Map<String, Value> = serde_json::from_str(
978 r#"{"policy": {"advisories": {"ignore": {"a/b": null}}, "malware": {"ignore-source": ["x"]}}}"#,
979 )
980 .expect("json");
981 let project: Map<String, Value> = serde_json::from_str(
982 r#"{"policy": {"advisories": {"ignore": {"c/d": "why"}, "block": false}, "abandoned": true, "malware": false}}"#,
983 )
984 .expect("json");
985 raw.merge(&global);
986 raw.merge(&project);
987 let p = PolicyConfig::from_raw(&raw).expect("policy");
988 assert!(!p.advisories.block);
989 let names: Vec<&str> = p
990 .advisories
991 .ignore
992 .iter()
993 .map(|(n, _)| n.as_str())
994 .collect();
995 assert_eq!(names, ["a/b", "c/d"]);
996 assert!(!p.malware.block);
997 assert!(!p.abandoned.block);
998 assert!(p.custom_lists.is_empty());
999 }
1000
1001 #[test]
1002 fn audit_ignore_lists_concatenate() {
1003 let mut raw = RawPolicyConfig::default();
1004 let g: Map<String, Value> =
1005 serde_json::from_str(r#"{"audit": {"ignore": ["CVE-1"]}}"#).expect("json");
1006 let p: Map<String, Value> =
1007 serde_json::from_str(r#"{"audit": {"ignore": {"CVE-2": "r"}}}"#).expect("json");
1008 raw.merge(&g);
1009 raw.merge(&p);
1010 let cfg = PolicyConfig::from_raw(&raw).expect("policy");
1011 let ids: Vec<&str> = cfg
1012 .advisories
1013 .ignore_id
1014 .iter()
1015 .map(|r| r.id.as_str())
1016 .collect();
1017 assert_eq!(ids, ["CVE-1", "CVE-2"]);
1018 }
1019}