Skip to main content

xensec_internal_tools/
xsagit.rs

1// Copyright 2021 Citrix
2// SPDX-License-Identifier: MIT OR Apache-2.0
3// There is NO WARRANTY.
4
5use crate::prelude::*;
6
7pub static MAIN_BRANCH: &str = "master";
8pub static MAIN_REMOTE: &str = "origin";
9
10pub type XsaNum = u32;
11pub type XsaVsn = u32;
12
13pub struct Repo {
14  g2: git2::Repository,
15  cfg: Arc<Config>,
16}
17
18pub type Tagged = BTreeMap<XsaNum, XsaVsn>;
19
20#[derive(Debug,Clone,Serialize,Deserialize)]
21pub struct Parsed {
22  pub cves: Vec<CveName>,
23  pub public: bool,
24}
25
26impl Repo {
27  #[throws(AE)]
28  pub fn new(cfg: &Arc<Config>, cache: &mut cache::Cache) -> Self {
29    let g2 = git2::Repository::open(&cfg.xsa_git)
30      .context("open xsa.git repo")?;
31
32    let lockfile = format!("{}/.git/xensec.lock", &cfg.xsa_git);
33    (||{
34      let lock = File::create(&lockfile).context("open/create")?;
35      lock.try_lock_exclusive().context("lock")?;
36      Box::leak(Box::new(lock));
37      Ok::<_,AE>(())
38    })()
39     .with_context(|| lockfile.clone())
40     .context("acquire xensec tools mutual exclusion lock")?;
41
42    let mut repo = Repo { g2, cfg: cfg.clone() };
43
44    let did1 = cache.run_git_fetch_pull(
45      RefreshKind::XsaGit, &mut repo,
46      &["fetch",
47        "--prune"],
48      MAIN_REMOTE,
49      &[MAIN_BRANCH],
50      &[&{ let t="refs/tags/*"; format!("{}:{}",t,t) }],
51    )
52      .context("fetch xsa.git")?;
53
54    let did2 = cache.run_git_fetch_pull(
55      RefreshKind::XsaGit, &mut repo,
56      &["merge",
57        "--ff-only"],
58      &format!("refs/remotes/{}/{}", MAIN_REMOTE, MAIN_BRANCH),
59      &[],
60      &[],
61    )
62      .context("update xsa.git")?;
63
64    assert_eq!(did1,did2);
65    cache.did_refresh(did1);
66
67    repo
68  }
69
70  #[throws(AE)]
71  pub fn head_commitid(&self) -> String {
72    self.g2().refname_to_id("HEAD")
73      .context("look up HEAD")?
74      .to_string()
75  }
76
77  #[throws(AE)]
78  pub fn list_by_tags(&self) -> Tagged {
79    let tags = self.g2
80      .tag_names(Some("xsa-*")).context("list tagw")?;
81    let mut xsas = tags.into_iter()
82      .filter_map(|maybe_tagname| {
83        let mut pieces = maybe_tagname?.splitn(3,'-');
84        assert_eq!( pieces.next(), Some("xsa"));
85        let xsa = pieces.next()?.parse().ok()?;
86        let vsn = pieces.next()?.strip_prefix("v")?.parse().ok()?;
87        Some((xsa,vsn))
88      }).collect_vec();
89    xsas.sort();
90    xsas.into_iter().group_by(|t| t.0)
91      .into_iter().map(|(_,vsns)| vsns.max().unwrap())
92      .collect()
93  }
94
95  #[throws(AE)]
96  pub fn parse_advisory(&self, xsa: XsaNum, vsn: XsaVsn) -> Parsed {
97    (||{
98      let std::process::Output { status, stdout, stderr:_ } = Command::new
99        ("Infra/parse-advisory")
100        .args(&[
101          format!("-v{}", vsn),
102          xsa.to_string(),
103        ])
104        .current_dir(&self.cfg.xsa_git)
105        .output().context("spawn")?;
106
107      if ! status.success() {
108        throw!(anyhow!("failed, wait status {:?}", status));
109      }
110      let json = serde_json::from_slice(&stdout)
111        .context("parse as JSON")?;
112
113      #[derive(Debug,Deserialize)]
114      #[allow(non_snake_case)]
115      struct Extract {
116        CVE: Option<String>,
117        EmbargoTime: Option<String>,
118        Withdrawn: Option<u8>,
119      }
120
121      let extract: Extract = serde_json::from_value(json)
122        .context("interpret JSON")?;
123
124      let cves: Vec<CveName> =
125        extract.CVE
126        .unwrap_or_default()
127        .split(',')
128        .map(|s| s.parse())
129        .collect::<Result<Vec<_>,_>>()
130        .context("parse CVE list")?;
131
132      let public = extract.EmbargoTime.is_none();
133      let _withdrawn = extract.Withdrawn.unwrap_or_default() != 0;
134      // Not sure what to do about withdrawn, let's just ignore it for now
135
136      Ok::<_,AE>(Parsed { cves, public })
137    })().with_context(|| format!("parse XSA-{} v{}", xsa, vsn))?
138  }
139
140  #[throws(AE)]
141  /// Leaves the JSON in xsa.git, wherefrom someone ought to read and delete
142  pub fn generate_cve_json(&self, xsa: XsaNum, xstate: &analysis::XsaState)
143                           -> BTreeMap<CveName, String /* abs path */>
144  {
145    (||{
146      let jpaths = xstate.cves.keys().map(
147        |cve| (cve.clone(), format!("{}/{}.json", &self.cfg.xsa_git, cve))
148      ).collect::<BTreeMap<_,_>>();
149
150      for f in jpaths.values() {
151        match fs::remove_file(f) {
152          Err(e) if e.kind() == ErrorKind::NotFound => { },
153          x => x.with_context(|| f.to_owned()).context("remove, prep")?,
154        }
155      }
156
157      let st = Command::new("Infra/parse-advisory")
158        .args(&[
159          "-CC",
160          &format!("-v{}", xstate.vsn),
161          &xsa.to_string(),
162        ])
163        .current_dir(&self.cfg.xsa_git)
164        .stdout(std::process::Stdio::null())
165        .status().context("run parse-advisory")?;
166      if ! st.success() {
167        throw!(anyhow!("parse-advisory -CC failed: {:?}", st))
168      }
169
170      for f in jpaths.values() {
171        fs::metadata(f).with_context(|| f.to_owned()).context("check")?;
172      }
173
174      Ok::<_,AE>(jpaths)
175    })()
176      .with_context(|| format!("generate CVE JSON for XSA-{}-v{}",
177                               xsa, xstate.vsn))?
178  }
179
180}
181
182impl githelp::Repo for Repo {
183  fn g2(&self) -> &git2::Repository { &self.g2 }
184  fn cfg(&self) -> &Config { &self.cfg }
185  fn dir(&self) -> &str { &self.cfg.xsa_git }
186  fn def_refname(&self) -> &str { "HEAD" }
187}