Skip to main content

just_shield/
advisory.rs

1//! 알려진 감염 버전 권고 DB (R9).
2//!
3//! 스냅숏은 컴파일 시점에 바이너리에 동봉된다(`include_str!`) — 런타임 파일/네트워크
4//! 의존이 없고, DB 갱신은 곧 새 릴리스이므로 DB만 바꿔치기하는 공격면이 없다.
5//! 형식과 갱신 절차는 `data/advisories.txt` 머리말 참조.
6
7use std::collections::HashMap;
8
9const BUNDLED: &str = include_str!("../data/advisories.txt");
10
11/// 권고 항목 — 출처와 설명.
12pub struct Advisory {
13    pub source: String,
14    pub note: String,
15}
16
17/// 참조(`owner/repo@ref`, 소문자) → 권고.
18pub struct AdvisoryDb {
19    entries: HashMap<String, Advisory>,
20}
21
22impl AdvisoryDb {
23    /// 동봉 스냅숏을 파싱한다.
24    pub fn bundled() -> Self {
25        Self::parse(BUNDLED)
26    }
27
28    pub fn parse(content: &str) -> Self {
29        let mut entries = HashMap::new();
30        for line in content.lines() {
31            let line = line.trim();
32            if line.is_empty() || line.starts_with('#') {
33                continue;
34            }
35            let mut parts = line.splitn(3, char::is_whitespace);
36            let (Some(reference), Some(source)) = (parts.next(), parts.next()) else {
37                continue;
38            };
39            entries.insert(
40                reference.to_lowercase(),
41                Advisory {
42                    source: source.to_string(),
43                    note: parts.next().unwrap_or("").trim().to_string(),
44                },
45            );
46        }
47        Self { entries }
48    }
49
50    /// `repo@ref`가 권고에 등재되어 있는가.
51    pub fn lookup(&self, repo: &str, git_ref: &str) -> Option<&Advisory> {
52        self.entries
53            .get(&format!("{repo}@{git_ref}").to_lowercase())
54    }
55
56    pub fn len(&self) -> usize {
57        self.entries.len()
58    }
59
60    pub fn is_empty(&self) -> bool {
61        self.entries.is_empty()
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::AdvisoryDb;
68
69    #[test]
70    fn bundled_snapshot_parses_and_contains_real_world_entry() {
71        let db = AdvisoryDb::bundled();
72        assert!(!db.is_empty());
73        let advisory = db
74            .lookup(
75                "tj-actions/changed-files",
76                "0e58ed8671d6b60d0890c21b07f8835ace038e67",
77            )
78            .expect("실세계 권고(CVE-2025-30066)가 동봉돼야 한다");
79        assert_eq!(advisory.source, "CVE-2025-30066");
80    }
81
82    #[test]
83    fn lookup_is_case_insensitive_and_misses_are_none() {
84        let db = AdvisoryDb::parse("Evil/Action@V1.0.0 GHSA-test 설명 텍스트\n# 주석\n");
85        assert!(db.lookup("evil/action", "v1.0.0").is_some());
86        assert!(db.lookup("evil/action", "v1.0.1").is_none());
87        assert!(db.lookup("good/action", "v1.0.0").is_none());
88    }
89}