nvd_cpe/
lib.rs

1//! [![github]](https://github.com/emo-crab/nvd-rs) [![crates-io]](https://crates.io/crates/nvd-cpe) [![docs-rs]](crate)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
6//!
7//! Official Common Platform Enumeration (CPE) Dictionary
8//!
9//!  CPE is a structured naming scheme for information technology systems, software, and packages. Based upon the generic syntax for Uniform Resource Identifiers (URI), CPE includes a formal name format, a method for checking names against a system, and a description format for binding text and tests to a name.
10//!  Below is the current official version of the CPE Product Dictionary. The dictionary provides an agreed upon list of official CPE names. The dictionary is provided in XML format and is available to the general public. Please check back frequently as the CPE Product Dictionary will continue to grow to include all past, present and future product releases. The CPE Dictionary is updated nightly when modifications or new names are added.
11//!
12//! As of December 2009, The National Vulnerability Database is now accepting contributions to the Official CPE Dictionary. Organizations interested in submitting CPE Names should contact the NVD CPE team at cpe_dictionary@nist.gov for help with the processing of their submission.
13//!
14//! The CPE Dictionary hosted and maintained at NIST may be used by nongovernmental organizations on a voluntary basis and is not subject to copyright in the United States. Attribution would, however, be appreciated by NIST.
15//!
16
17#![doc(html_root_url = "https://emo-crab.github.io/nvd-rs/cpe")]
18
19// Package wfn provides a representation, bindings and matching of the Well-Formed CPE names as per
20// https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7695.pdf and
21// https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7696.pdf
22use serde::{Deserialize, Serialize};
23use std::collections::HashSet;
24use std::{fmt, str::FromStr};
25
26pub mod component;
27pub mod dictionary;
28pub mod error;
29pub mod part;
30
31use crate::component::Language;
32use crate::error::{CPEError, Result};
33use component::Component;
34use part::Part;
35
36// https://csrc.nist.gov/projects/security-content-automation-protocol/specifications/cpe
37// view-source:https://csrc.nist.gov/schema/cpe/2.3/cpe-dictionary_2.3.xsd
38// https://scap.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd
39// cpe:2.3:part:vendor:product:version:update:edition:language:sw_edition:target_sw: target_hw:other
40// CPE属性
41#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
42#[serde(deny_unknown_fields)]
43pub struct CPEName {
44  // 分类:a,o,h
45  pub part: Part,
46  // 创建产品个人或者组织/厂商
47  pub vendor: Component,
48  // 产品标题或者名称
49  pub product: Component,
50  // 由厂商提供用来表示产品的特定的发行版本
51  pub version: Component,
52  // 同样是厂商提供表示产品的更新版本,比version范围更小
53  pub update: Component,
54  // 这个属性同样表示版本,属于被弃用的属性,一般是为了兼容更早CPE版本,默认值为ANY
55  pub edition: Component,
56  // 表示产品在操作界面所支持的语言
57  pub language: Language,
58  // 表示产品是针对某些特定市场或类别的目标用户
59  pub sw_edition: Component,
60  // 产品运行需要的软件环境
61  pub target_sw: Component,
62  // 产品运行需要的硬件环境
63  pub target_hw: Component,
64  // 表示无法归类上上述其他属性的值
65  pub other: Component,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Hash)]
69pub struct Product {
70  pub part: String,
71  pub vendor: String,
72  pub product: String,
73}
74
75impl From<&CPEName> for Product {
76  fn from(val: &CPEName) -> Self {
77    Product {
78      part: val.part.to_string(),
79      vendor: val.vendor.to_string(),
80      product: val.product.to_string(),
81    }
82  }
83}
84
85impl CPEName {
86  // 从uri转CPE属性
87  pub fn from_uri(uri: &str) -> Result<Self> {
88    let uri = match uri.strip_prefix("cpe:2.3:") {
89      Some(u) => u,
90      None => {
91        return Err(CPEError::InvalidPrefix {
92          value: uri.to_string(),
93        });
94      }
95    };
96
97    let mut components = uri.split(':');
98    let error = CPEError::InvalidPart {
99      value: uri.to_string(),
100    };
101    let part = Part::from_str(components.next().ok_or(&error)?)?;
102    let vendor = Component::from_str(components.next().ok_or(&error)?)?;
103    let product = Component::from_str(components.next().ok_or(&error)?)?;
104    let version = Component::from_str(components.next().ok_or(&error)?)?;
105    let update = Component::from_str(components.next().ok_or(&error)?)?;
106    let edition = Component::from_str(components.next().ok_or(&error)?)?;
107    let language = Language::from_str(components.next().ok_or(&error)?)?;
108    let sw_edition = Component::from_str(components.next().ok_or(&error)?)?;
109    let target_sw = Component::from_str(components.next().ok_or(&error)?)?;
110    let target_hw = Component::from_str(components.next().ok_or(&error)?)?;
111    let other = Component::from_str(components.next().ok_or(&error)?)?;
112
113    Ok(Self {
114      part,
115      vendor,
116      product,
117      version,
118      update,
119      edition,
120      language,
121      sw_edition,
122      target_sw,
123      target_hw,
124      other,
125    })
126  }
127  // 从wfn转CPE属性
128  pub fn from_wfn(name: &str) -> Result<Self> {
129    let prefix = match name.strip_prefix("wfn:[") {
130      Some(u) => u,
131      None => {
132        return Err(CPEError::InvalidPrefix {
133          value: name.to_string(),
134        });
135      }
136    };
137    let components = match prefix.strip_suffix(']') {
138      Some(u) => u,
139      None => {
140        return Err(CPEError::InvalidPrefix {
141          value: name.to_string(),
142        });
143      }
144    };
145    let mut att = CPEName {
146      part: Part::default(),
147      vendor: Default::default(),
148      product: Default::default(),
149      version: Default::default(),
150      update: Default::default(),
151      edition: Default::default(),
152      language: Default::default(),
153      sw_edition: Default::default(),
154      target_sw: Default::default(),
155      target_hw: Default::default(),
156      other: Default::default(),
157    };
158    let mut verify_set = HashSet::from([
159      "part",
160      "vendor",
161      "product",
162      "version",
163      "update",
164      "edition",
165      "language",
166      "sw_edition",
167      "target_sw",
168      "target_hw",
169      "other",
170    ]);
171    for component in components.split(',') {
172      match component.split_once('=') {
173        None => {
174          return Err(CPEError::InvalidPart {
175            value: component.to_string(),
176          });
177        }
178        Some((k, v)) => {
179          match k {
180            "part" => att.part = Part::from_str(v)?,
181            "vendor" => att.vendor = Component::from_str(v)?,
182            "product" => att.product = Component::from_str(v)?,
183            "version" => att.version = Component::from_str(v)?,
184            "update" => att.update = Component::from_str(v)?,
185            "edition" => att.edition = Component::from_str(v)?,
186            "language" => att.language = Language::from_str(v)?,
187            "sw_edition" => att.sw_edition = Component::from_str(v)?,
188            "target_sw" => att.target_sw = Component::from_str(v)?,
189            "target_hw" => att.target_hw = Component::from_str(v)?,
190            "other" => att.other = Component::from_str(v)?,
191            _ => {
192              return Err(CPEError::InvalidPart {
193                value: k.to_string(),
194              });
195            }
196          }
197          // double
198          if !verify_set.remove(k) {
199            return Err(CPEError::InvalidPart {
200              value: k.to_string(),
201            });
202          }
203        }
204      }
205    }
206    if !verify_set.is_empty() {
207      return Err(CPEError::InvalidWfn {
208        value: name.to_string(),
209      });
210    }
211    Ok(att)
212  }
213}
214
215impl FromStr for CPEName {
216  type Err = CPEError;
217  fn from_str(uri: &str) -> Result<Self> {
218    CPEName::from_uri(uri)
219  }
220}
221
222impl fmt::Display for CPEName {
223  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224    let Self {
225      part,
226      vendor,
227      product,
228      version,
229      update,
230      edition,
231      language,
232      sw_edition,
233      target_sw,
234      target_hw,
235      other,
236    } = self;
237
238    write!(
239            f,
240            "cpe:2.3:{part:#}:{vendor}:{product}:{version}:{update}:{edition}:{language}:{sw_edition}:{target_sw}:{target_hw}:{other}",
241        )
242  }
243}
244
245fn strip_slashes(s: &str) -> String {
246  let mut out = String::new();
247  let mut chats = s.chars();
248  while let Some(c) = chats.next() {
249    if c == '\\' {
250      if let Some(cc) = chats.next() {
251        match cc {
252          '\\' => {
253            continue;
254          }
255          _ => {
256            out.push(cc);
257          }
258        }
259      }
260    } else {
261      out.push(c);
262    }
263  }
264  out
265}
266
267fn parse_uri_attribute(value: &str) -> Result<String> {
268  let value = if value.contains("%01") || value.contains("%02") {
269    let value = value.replace("%01", "?").replace("%02", "*");
270    percent_encoding::percent_decode_str(&value)
271      .decode_utf8()
272      .map_err(|source| CPEError::Utf8Error {
273        source,
274        value: value.to_owned(),
275      })?
276      .to_string()
277  } else {
278    percent_encoding::percent_decode_str(value)
279      .decode_utf8()
280      .map_err(|source| CPEError::Utf8Error {
281        source,
282        value: value.to_owned(),
283      })?
284      .to_string()
285  };
286  let value = strip_slashes(value.as_str());
287  Ok(value)
288}
289
290pub fn version_cmp(a: &str, b: &str, operator: &str) -> bool {
291  if let Ok(op) = version_compare::Cmp::from_sign(operator) {
292    if let Ok(res) = version_compare::compare_to(a, b, op) {
293      return res;
294    }
295  }
296  false
297}
298
299impl CPEName {
300  // 匹配指定版本是否存在漏洞
301  pub fn match_version(&self, version: &str) -> bool {
302    if self.version.is_any() {
303      return true;
304    } else if self.version.is_na() {
305      return false;
306    }
307    let my_version = if self.update.is_value() {
308      format!("{} {}", self.version, self.update)
309    } else {
310      self.version.to_string()
311    };
312    version_cmp(version, &my_version, "==")
313  }
314  // 是否匹配指定产品
315  pub fn match_product(&self, product: &str) -> bool {
316    if self.product.is_any() {
317      return true;
318    } else if self.product.is_na() {
319      return false;
320    }
321    product == self.normalize_target_software()
322  }
323  // 规范化目标软件,
324  fn normalize_target_software(&self) -> String {
325    if let Component::Value(software) = &self.target_sw {
326      format!("{}-{}", software, self.product)
327    } else {
328      self.product.to_string()
329    }
330  }
331}