Skip to main content

xensec_internal_tools/
cvelist.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 UPSTREAM_MAIN_BRANCH: &str = "master";
8
9pub struct Local {
10  g2: git2::Repository,
11  remote_head_refname: String,
12  cfg: Arc<Config>,
13}
14
15#[derive(Copy,Clone,Debug,Hash,Eq,PartialEq,Ord,PartialOrd)]
16#[derive(EnumString)]
17#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
18pub enum CveListState {
19  #[strum(disabled)] Pending,
20  Reserved,
21  Public,
22}
23
24#[ext(pub)]
25impl XsaNum {
26  fn cvelist_branch_name(self) -> String { format!("xsa-{}", self) }
27
28  fn title_with_cves(self, cves: &mut dyn Iterator<Item=&CveName>) -> String {
29    let mut m = String::new();
30    write!(m, "XSA-{}", self).unwrap();
31    for cve in cves { write!(m, " {}", cve).unwrap(); }
32    m
33  }
34}
35
36impl Local {
37  #[throws(AE)]
38  pub fn new(cfg: &Arc<Config>, cache: &mut cache::Cache) -> Self {
39
40    let g2 = git2::Repository::open(&cfg.cvelist_git)
41      .context("open cvelist.git repo")?;
42
43    let remote_head_refname = format!(
44      "refs/remotes/{}/{}",
45      cfg.cvelist_remote_upstream,
46      UPSTREAM_MAIN_BRANCH
47    );
48
49    let mut repo = Local { g2, remote_head_refname, cfg: cfg.clone() };
50
51    let mut fetch_1 = |kind, remote_name: &'_ str, branch_pat: &'_ _| {
52      let did = cache.run_git_fetch_pull(
53        kind, &mut repo,
54        &["fetch",
55          "--prune",
56          "--no-tags"],
57        remote_name,
58        &[branch_pat],
59        &[],
60      )
61        .with_context(|| format!("remote={} branches={}",
62                                 remote_name, branch_pat))?;
63      cache.did_refresh(did);
64      Ok::<_,AE>(())
65    };
66      
67    fetch_1(RefreshKind::CveListUpstream,
68            &cfg.cvelist_remote_upstream,
69            UPSTREAM_MAIN_BRANCH)?;
70            
71    fetch_1(RefreshKind::CveListXenSec,
72            &cfg.cvelist_remote_xensec,
73            "*")?;
74
75    repo
76  }
77
78  pub fn cve_path(&self, cve: &CveName) -> String {
79    let mut pieces = cve.as_str().split('-').skip(1);
80    let year: u32 = pieces.next().unwrap().parse().unwrap();
81    let num : u32 = pieces.next().unwrap().parse().unwrap();
82    format!("{}/{}xxx/{}.json", year, num/1000, cve)
83  }
84
85  #[throws(AE)]
86  pub fn cve_data_raw(&self, refspec: RefSpec, cve: &CveName)
87                      -> Option<serde_json::Value>
88  {
89    let path = self.cve_path(cve);
90    (||{
91      let data = match self.file(refspec, &path).context("get file")? {
92        Some(x) => x,
93        None => return Ok(None),
94      };
95      let data = serde_json::from_str(&data).context("parse as JSON")?;
96      Ok::<_,AE>(Some(data))
97    })().with_context(|| format!("get cvelist data for {} from {:?}",
98                                 &cve, path))?
99  }
100
101  #[throws(AE)]
102  pub fn cve_state(&self, refspec: RefSpec, cve: &CveName)
103                   -> Option<CveListState>
104  {
105    let data = match self.cve_data_raw(refspec, cve)? {
106      None => return None,
107      Some(x) => x,
108    };
109
110    let cvelist_state = data
111      .as_object()          .ok_or_else(|| anyhow!("root not object"))?
112      .get("CVE_data_meta") .ok_or_else(|| anyhow!("no CVE_data_meta"))?;
113
114    #[derive(Deserialize,Debug)]
115    #[allow(non_snake_case)]
116    struct M {
117      ID: String,
118      ASSIGNER: String,
119      STATE: Option<String>,
120    }
121
122    let m: M = serde_json::from_value(cvelist_state.clone())
123      .context("deserialize CVE_data_meta")?;
124
125    if m.ID != cve.as_str() {
126      throw!(anyhow!("CVE_data_meta ID is wrong! {:?}", &m));
127    }
128
129    if let Some(state) = m.STATE {
130      Some(state.parse().context("parse")?)
131    } else if &m.ASSIGNER == &self.cfg.committer_email {
132      Some(CveListState::Pending)
133    } else {
134      throw!(anyhow!(
135        "inexplicable CVE_data_meta (no STATE, ASSIGNER is {:?} not us {:?}: \
136         {:?}", &m.ASSIGNER, &self.cfg.committer_email, &m
137      ))
138    }
139  }
140
141  pub fn xsa_mr_branch_refname(&self, xsa: XsaNum) -> String {
142    format!("refs/remotes/{}/{}",
143            &self.cfg.cvelist_remote_xensec,
144            &xsa.cvelist_branch_name())
145  }
146
147  #[throws(AE)]
148  pub fn remote_url(&self, remote_name: &str) -> String {
149    (||{
150      let remote = self.g2.find_remote(remote_name).context("find remote")?;
151      let url = remote.url().ok_or_else(|| anyhow!("invalid UTF-8 in url"))?;
152      Ok::<_,AE>(url.to_owned())
153    })()
154      .with_context(|| format!("remote {:?}", remote_name))?
155  }
156
157  #[throws(AE)]
158  pub fn make_xsajson_commit(&self, xsa_git: &xsagit::Repo,
159                             xsa: XsaNum, vsn: XsaVsn,
160                             jpaths: &BTreeMap<CveName,String>)
161                             -> git2::Oid
162  {
163    let base_commitid = self.g2()
164      .refname_to_id(&self.remote_head_refname)
165      .context("look up base commitid")?;
166    let base_commit = self.g2().find_commit(base_commitid)
167      .context("find base commit")?;
168    let base_tree = base_commit.tree()
169      .context("get ase tree")?;
170    let mut builder = git2::build::TreeUpdateBuilder::new();
171    
172    for (cve,jpath) in jpaths {
173      let json_blob = self.g2()
174        .blob_path(jpath.as_ref())
175        .with_context(|| jpath.to_string())
176        .context("construct and write json blob")?;
177
178      builder.upsert(
179        self.cve_path(cve),
180        json_blob,
181        git2::FileMode::Blob,
182      );
183    }
184
185    let new_tree = builder.create_updated(self.g2(), &base_tree)
186      .context("build new tree")?;
187    let new_tree = self.g2().find_tree(new_tree).context("find new tree")?;
188
189    let signature = git2::Signature::now(
190      &self.cfg.committer_name,
191      &self.cfg.committer_email,
192    ).context("make signature line")?;
193    
194    let xsagit_head = xsa_git.head_commitid()?;
195
196    let message = (||{
197      let mut m = xsa.title_with_cves(&mut jpaths.keys());
198      write!(m, "\n\n")?;
199      writeln!(m, "Xensec source data: xsa.git#xsa-{}-v{}", xsa, vsn)?;
200      writeln!(m, "Xensec source infra: xsa.git#{}", xsagit_head)?;
201      Ok::<_,fmt::Error>(m)
202    })().unwrap();
203
204    let new_commit = self.g2().commit(
205      None, &signature, &signature, &message,
206      &new_tree, &[&base_commit]
207    ).context("make new commit")?;
208
209    new_commit
210  }
211}
212
213impl githelp::Repo for Local {
214  fn g2(&self) -> &git2::Repository { &self.g2 }
215  fn cfg(&self) -> &Config { &self.cfg }
216  fn dir(&self) -> &str { &self.cfg.cvelist_git }
217  fn def_refname(&self) -> &str { &self.remote_head_refname }
218}