Compare commits

...

10 Commits

Author SHA1 Message Date
Jia Chao
772770911b SaInfo, CVE: add derive Eq, PartialEq, Hash
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-19 15:25:52 +08:00
Jia Chao
5403c5a6d6 fix: make SaInfo->cves public
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-19 15:19:59 +08:00
Jia Chao
284803e2bd pub XmlReader
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-13 18:02:01 +08:00
Jia Chao
fcdab1f40b CVRF: 添加查询与转换功能
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-12 14:27:55 +08:00
Jia Chao
61a06e5ba6 improve: note 和 reference 使用 hashmap 存储,更方便查询
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-12 11:02:03 +08:00
Jia Chao
524b23e0d8 fix: reference 可能包含多个 url 字段
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-12 10:43:17 +08:00
Jia Chao
809d87897e 使用 enum 威胁等级 Severity
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-11 16:40:32 +08:00
Jia Chao
89c831a48b CVRF: 处理 vulnerabilities
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-11 15:34:02 +08:00
Jia Chao
973e22c89e CVRF: 处理 producttree
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-07 16:12:36 +08:00
Jia Chao
b70a48ce3c CVRF: 处理 documentreferences
Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
2024-06-07 15:08:16 +08:00
2 changed files with 634 additions and 23 deletions

View File

@ -3,8 +3,10 @@
allow(dead_code, unused_imports, unused_variables, unused_mut)
)]
use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::{self, BufReader};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use tracing::{debug, error, instrument, trace};
@ -14,7 +16,7 @@ use xml::reader::{EventReader, XmlEvent};
mod test;
#[allow(dead_code)]
struct XmlReader {
pub struct XmlReader {
// an iterator for XmlEvent
events: EventReader<BufReader<File>>,
@ -30,6 +32,10 @@ impl XmlReader {
XmlReader { events, depth: 0 }
}
pub fn depth(&self) -> usize {
self.depth
}
// pull next stream from xml, set the depth as well.
pub fn next(&mut self) -> Result<xml::reader::XmlEvent, xml::reader::Error> {
let event = self.events.next();
@ -95,7 +101,7 @@ impl XmlReader {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CVRF {
pub struct CVRF {
// <DocumentTitle xml:lang="en">
pub documenttitle: String,
@ -109,16 +115,16 @@ struct CVRF {
pub documenttracking: DocumentTracking,
// <DocumentNotes>
pub documentnotes: Vec<Note>,
pub documentnotes: HashMap<String, Note>,
// <DocumentReferences>
pub documentreferences: Vec<Reference>,
pub documentreferences: HashMap<String, Reference>,
// <ProductTree xmlns="http://www.icasi.org/CVRF/schema/prod/1.1">
pub producttree: ProductTree,
// <Vulnerability xmlns="http://www.icasi.org/CVRF/schema/vuln/1.1" Ordinal="1">
pub vulnerability: Vulnerability,
pub vulnerabilities: Vec<Vulnerability>,
}
impl CVRF {
@ -130,10 +136,93 @@ impl CVRF {
documenttype: String::new(),
documentpublisher: Publisher::new(),
documenttracking: DocumentTracking::new(),
documentnotes: vec![],
documentreferences: vec![],
documentnotes: HashMap::new(),
documentreferences: HashMap::new(),
producttree: ProductTree::new(),
vulnerability: Vulnerability::new(),
vulnerabilities: Vec::new(),
}
}
/// 获取安全公告的 ID
#[instrument(skip(self))]
pub fn id(&self) -> &str {
self.documenttracking.identification.id.as_str()
}
/// 获取安全公告的标题内容
#[instrument(skip(self))]
pub fn title(&self) -> &str {
&self.documenttitle
}
/// 安全公告中的 url 不正确,故重新组合一次
#[instrument(skip(self))]
pub fn url(&self) -> String {
format!("https://www.openeuler.org/zh/security/security-bulletins/detail/?id={}", self.id())
}
/// 获取安全公告的概要信息
#[instrument(skip(self))]
pub fn summary(&self) -> Option<&str> {
if let Some(note) = self.documentnotes.get("Summary") {
Some(note.content.as_str())
} else {
None
}
}
/// 获取安全公告的详细信息
#[instrument(skip(self))]
pub fn description(&self) -> Option<&str> {
if let Some(note) = self.documentnotes.get("Description") {
Some(note.content.as_str())
} else {
None
}
}
/// 获取安全公告的危害等级
#[instrument(skip(self))]
pub fn severity(&self) -> Result<Severity, ParseSeverityError> {
if let Some(note) = self.documentnotes.get("Severity") {
note.content.parse::<Severity>()
} else {
// 正常用不到这里
Ok(Severity::Null)
}
}
/// 列出受此安全公告影响的组件
#[instrument(skip(self))]
pub fn affected_component(&self) -> Option<&str> {
if let Some(note) = self.documentnotes.get("Affected Component") {
Some(note.content.as_str())
} else {
None
}
}
/// 列出受此安全公告影响的系统列表
#[instrument(skip(self))]
pub fn affected_products(&self) -> &Vec<Product> {
&self.producttree.products
}
/// 将之转换成精简的公告格式
#[instrument(skip(self))]
pub fn sainfo(&self) -> SaInfo {
let mut cves = vec![];
for v in &self.vulnerabilities {
cves.push(v.to_cve());
}
SaInfo {
id: self.id().to_string(),
url: self.url(),
title: self.title().to_string(),
severity: self.severity().unwrap(),
description: self.description().unwrap().to_string(),
cves,
}
}
@ -145,7 +234,7 @@ impl CVRF {
loop {
let event = xmlreader.next();
if xmlreader.depth != 2 {
if xmlreader.depth() != 2 {
if event == Ok(XmlEvent::EndDocument) {
trace!("End of the xml, break...");
break;
@ -161,6 +250,9 @@ impl CVRF {
"DocumentPublisher" => self.documentpublisher.load_from_xmlreader(xmlreader),
"DocumentTracking" => self.documenttracking.load_from_xmlreader(xmlreader),
"DocumentNotes" => self.handle_notes(xmlreader),
"DocumentReferences" => self.handle_references(xmlreader),
"ProductTree" => self.producttree.load_from_xmlreader(xmlreader),
"Vulnerability" => self.handle_vulnerabilities(xmlreader),
_ => {}
},
Err(e) => {
@ -179,10 +271,30 @@ impl CVRF {
let mut note = Note::new();
note.load_from_xmlreader(xmlreader);
if xmlreader.depth < 2 {
if xmlreader.depth() < 2 {
break;
}
self.documentnotes.push(note);
self.documentnotes.insert(note.title.clone(), note);
}
}
fn handle_references(&mut self, xmlreader: &mut XmlReader) {
loop {
let mut reference = Reference::new();
reference.load_from_xmlreader(xmlreader);
if xmlreader.depth() < 2 {
break;
}
self.documentreferences.insert(reference.r#type.clone(), reference);
}
}
fn handle_vulnerabilities(&mut self, xmlreader: &mut XmlReader) {
let mut vulnerability = Vulnerability::new();
vulnerability.load_from_xmlreader(xmlreader);
if vulnerability.cve != "" {
self.vulnerabilities.push(vulnerability);
}
}
}
@ -313,7 +425,7 @@ impl DocumentTracking {
let mut revision = Revision::new();
revision.load_from_xmlreader(xmlreader);
// 所有 revision 读取完毕
if xmlreader.depth < 3 {
if xmlreader.depth() < 3 {
trace!("RevisionHistory read to end.");
break;
}
@ -496,14 +608,40 @@ impl Note {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reference {
pub r#type: String,
pub url: String,
pub url: Vec<String>,
}
impl Reference {
pub fn new() -> Self {
Reference {
r#type: String::new(),
url: String::new(),
url: Vec::new(),
}
}
#[instrument(skip(self, xmlreader))]
fn load_from_xmlreader(&mut self, xmlreader: &mut XmlReader) {
loop {
match xmlreader.next() {
Ok(XmlEvent::StartElement { attributes, .. }) => {
if xmlreader.depth() == 3 {
self.r#type = attributes[0].value.clone();
} else {
self.url.push(xmlreader.next_characters());
}
}
Ok(XmlEvent::EndElement { .. }) => {
if xmlreader.depth() < 3 {
trace!("Reference read end.");
break;
}
}
Err(e) => {
error!("XmlReader Error: {e}");
break;
}
_ => {}
}
}
}
}
@ -547,6 +685,76 @@ impl ProductTree {
packages: HashMap::new(),
}
}
#[instrument(skip(self, xmlreader))]
fn load_from_xmlreader(&mut self, xmlreader: &mut XmlReader) {
let mut _type = String::new();
let mut _name = String::new();
loop {
match xmlreader.next() {
Ok(XmlEvent::StartElement { attributes, .. }) => {
for attr in attributes {
match attr.name.local_name.as_str() {
"Type" => {
_type = attr.value.clone();
}
"Name" => {
_name = attr.value.clone();
}
_ => {}
}
}
}
Ok(XmlEvent::EndElement { .. }) => {
if xmlreader.depth() < 2 {
trace!("ProductTree read end.");
break;
}
}
Err(e) => {
error!("XmlReader Error: {e}");
break;
}
_ => {}
}
if _type.as_str() == "Product Name" {
self._load_products_branch(xmlreader);
}
if _type.as_str() == "Package Arch" {
self.packages.insert(_name.clone(), vec![]);
self._load_packages_branch(&_name, xmlreader);
}
_type.clear();
_name.clear();
}
}
#[instrument(skip(self, xmlreader))]
fn _load_products_branch(&mut self, xmlreader: &mut XmlReader) {
loop {
let mut product = Product::new();
product.load_from_xmlreader(xmlreader);
if xmlreader.depth() < 3 {
break;
}
self.products.push(product);
}
}
#[instrument(skip(self, key, xmlreader))]
fn _load_packages_branch(&mut self, key: &str, xmlreader: &mut XmlReader) {
let packages = self.packages.get_mut(key).unwrap();
loop {
let mut package = Product::new();
package.load_from_xmlreader(xmlreader);
if xmlreader.depth() < 3 {
break;
}
packages.push(package);
}
}
}
// depth = 4
@ -573,6 +781,33 @@ impl Product {
content: String::new(),
}
}
#[instrument(skip(self, xmlreader))]
fn load_from_xmlreader(&mut self, xmlreader: &mut XmlReader) {
loop {
match xmlreader.next() {
Ok(XmlEvent::StartElement { attributes, .. }) => {
for attr in attributes {
match attr.name.local_name.as_str() {
"ProductID" => self.productid = attr.value.clone(),
"CPE" => self.cpe = attr.value.clone(),
_ => {}
}
}
self.content = xmlreader.next_characters();
}
Ok(XmlEvent::EndElement { .. }) => {
trace!("Product read end.");
break;
}
Err(e) => {
error!("XmlReader Error: {e}");
break;
}
_ => {}
}
}
}
}
// depth = 2
@ -588,7 +823,7 @@ impl Product {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
// <Notes>...</Notes>
pub notes: Vec<Note>,
pub notes: HashMap<String, Note>,
// <ReleaseDate>
pub releasedate: String,
@ -612,7 +847,7 @@ pub struct Vulnerability {
impl Vulnerability {
pub fn new() -> Self {
Vulnerability {
notes: Vec::new(),
notes: HashMap::new(),
releasedate: String::new(),
cve: String::new(),
productstatuses: Vec::new(),
@ -621,6 +856,97 @@ impl Vulnerability {
remediations: Vec::new(),
}
}
// 转换成简单的 CVE 格式
pub fn to_cve(&self) -> CVE {
let id = self.cve.clone();
let url = format!("https://nvd.nist.gov/vuln/detail/{}", self.cve);
let severity = self.threats[0].description.clone();
CVE {
id,
url,
severity,
}
}
#[instrument(skip(self, xmlreader))]
fn load_from_xmlreader(&mut self, xmlreader: &mut XmlReader) {
loop {
let key = if let Some(key) = xmlreader.next_start_name_under_depth(1) {
key
} else {
break;
};
match key.as_str() {
"Notes" => self.handle_notes(xmlreader),
"ReleaseDate" => self.releasedate = xmlreader.next_characters(),
"CVE" => self.cve = xmlreader.next_characters(),
"ProductStatuses" => self.handle_productstatuses(xmlreader),
"Threats" => self.handle_threats(xmlreader),
"CVSSScoreSets" => self.handle_cvssscoresets(xmlreader),
"Remediations" => self.handle_remediations(xmlreader),
_ => {}
}
}
}
fn handle_notes(&mut self, xmlreader: &mut XmlReader) {
loop {
let mut note = Note::new();
note.load_from_xmlreader(xmlreader);
if xmlreader.depth() < 3 {
break;
}
self.notes.insert(note.title.clone(), note);
}
}
fn handle_productstatuses(&mut self, xmlreader: &mut XmlReader) {
loop {
let mut status = ProductStatus::new();
status.load_from_xmlreader(xmlreader);
if xmlreader.depth() < 3 {
break;
}
self.productstatuses.push(status);
}
}
fn handle_threats(&mut self, xmlreader: &mut XmlReader) {
loop {
let mut threat = Threat::new();
threat.load_from_xmlreader(xmlreader);
if xmlreader.depth() < 3 {
break;
}
self.threats.push(threat);
}
}
fn handle_cvssscoresets(&mut self, xmlreader: &mut XmlReader) {
loop {
let mut scoreset = ScoreSet::new();
scoreset.load_from_xmlreader(xmlreader);
if xmlreader.depth() < 3 {
break;
}
self.cvssscoresets.push(scoreset);
}
}
fn handle_remediations(&mut self, xmlreader: &mut XmlReader) {
loop {
let mut remediation = Remediation::new();
remediation.load_from_xmlreader(xmlreader);
if xmlreader.depth() < 3 {
break;
}
self.remediations.push(remediation);
}
}
}
// depth = 4
@ -644,6 +970,30 @@ impl ProductStatus {
products: Vec::new(),
}
}
#[instrument(skip(self, xmlreader))]
fn load_from_xmlreader(&mut self, xmlreader: &mut XmlReader) {
loop {
match xmlreader.next() {
Ok(XmlEvent::StartElement { attributes, .. }) => {
if xmlreader.depth() == 4 {
self.status = attributes[0].value.clone();
}
self.products.push(xmlreader.next_characters());
}
Ok(XmlEvent::EndElement { .. }) => {
if xmlreader.depth() < 4 {
break;
}
}
Err(e) => {
error!("XmlReader Error: {e}");
break;
}
_ => {}
}
}
}
}
// depth = 4
@ -656,17 +1006,90 @@ pub struct Threat {
pub r#type: String,
// As threat level
pub description: String,
pub description: Severity,
}
impl Threat {
pub fn new() -> Self {
Threat {
r#type: String::new(),
description: String::new(),
description: Severity::new(),
}
}
#[instrument(skip(self, xmlreader))]
fn load_from_xmlreader(&mut self, xmlreader: &mut XmlReader) {
loop {
match xmlreader.next() {
Ok(XmlEvent::StartElement { attributes, .. }) => {
if xmlreader.depth() == 4 {
self.r#type = attributes[0].value.clone();
} else {
self.description = xmlreader.next_characters().parse::<Severity>().unwrap();
}
}
Ok(XmlEvent::EndElement { .. }) => {
if xmlreader.depth() < 4 {
break;
}
}
Err(e) => {
error!("XmlReader Error: {e}");
break;
}
_ => {}
}
}
}
}
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Severity {
Null,
Low,
Moderate,
Important,
Critical,
}
impl Severity {
pub fn new() -> Self {
Severity::Null
}
}
// 为枚举 Severity 实现 FromStr trait
impl FromStr for Severity {
type Err = ParseSeverityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"low" => Ok(Severity::Low),
"moderate" | "medium" => Ok(Severity::Moderate),
"important" | "high" => Ok(Severity::Important),
"critical" => Ok(Severity::Critical),
_ => Err(ParseSeverityError::InvalidSeverity),
}
}
}
// 定义 ParseSeverityError 枚举类型来表示解析错误
#[derive(Debug, Clone)]
pub enum ParseSeverityError {
InvalidSeverity,
}
// 为 ParseSeverityError 实现 Display trait以便更好地显示错误信息
impl fmt::Display for ParseSeverityError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParseSeverityError::InvalidSeverity => write!(f, "Invalid severity level"),
}
}
}
// 为 ParseSeverityError 实现 std::error::Error trait
impl std::error::Error for ParseSeverityError {}
// depth = 4
// <ScoreSet>
@ -689,6 +1112,23 @@ impl ScoreSet {
vector: String::new(),
}
}
#[instrument(skip(self, xmlreader))]
fn load_from_xmlreader(&mut self, xmlreader: &mut XmlReader) {
loop {
let key = if let Some(key) = xmlreader.next_start_name_under_depth(3) {
key
} else {
break;
};
match key.as_str() {
"BaseScore" => self.basescore = xmlreader.next_characters(),
"Vector" => self.vector = xmlreader.next_characters(),
_ => {}
}
}
}
}
// depth = 4
@ -723,4 +1163,77 @@ impl Remediation {
url: String::new(),
}
}
#[instrument(skip(self, xmlreader))]
fn load_from_xmlreader(&mut self, xmlreader: &mut XmlReader) {
// 读取类型
loop {
match xmlreader.next() {
Ok(XmlEvent::StartElement { attributes, .. }) => {
if xmlreader.depth() == 4 {
self.r#type = attributes[0].value.clone();
}
break;
}
Ok(XmlEvent::EndElement { .. }) => {
if xmlreader.depth() < 4 {
break;
}
}
Err(e) => {
error!("XmlReader Error: {e}");
break;
}
_ => {}
}
}
// 其它字段
loop {
let key = if let Some(key) = xmlreader.next_start_name_under_depth(3) {
key
} else {
break;
};
match key.as_str() {
"Description" => self.description = xmlreader.next_characters(),
"DATE" => self.date = xmlreader.next_characters(),
"URL" => self.url = xmlreader.next_characters(),
_ => {}
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct SaInfo {
// sa id
pub id: String,
// sa's url
pub url: String,
// sa title
pub title: String,
// the major severity
pub severity: Severity,
pub description: String,
// 包含的 cve 列表
pub cves: Vec<CVE>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct CVE {
// cve id
pub id: String,
// cve 官网地址
pub url: String,
// 严重级别
pub severity: Severity,
}

View File

@ -42,10 +42,108 @@ fn cvrf_works() {
let note_title = "Synopsis";
let note_type = "General";
let note_ordinal = "1";
let note_content = "An update for golang is now available for openEuler-20.03-LTS-SP1,openEuler-20.03-LTS-SP4,openEuler-22.03-LTS,openEuler-22.03-LTS-SP1,openEuler-22.03-LTS-SP2 and openEuler-22.03-LTS-SP3.";
let note_content = "golang security update";
assert_eq!(cvrf.documentnotes.len(), 6);
assert_eq!(cvrf.documentnotes[0].title, note_title);
assert_eq!(cvrf.documentnotes[0].r#type, note_type);
assert_eq!(cvrf.documentnotes[0].ordinal, note_ordinal);
assert_eq!(cvrf.documentnotes[1].content, note_content);
if let Some(note) = cvrf.documentnotes.get("Synopsis") {
assert_eq!(note.title, note_title);
assert_eq!(note.r#type, note_type);
assert_eq!(note.ordinal, note_ordinal);
assert_eq!(note.content, note_content);
} else {
assert!(false);
}
// references
let reference_type = "openEuler CVE";
let reference_url = "https://www.openeuler.org/en/security/cve/detail.html?id=CVE-2023-45288";
assert_eq!(cvrf.documentreferences.len(), 3);
if let Some(reference) = cvrf.documentreferences.get("openEuler CVE") {
assert_eq!(reference.r#type, reference_type);
assert_eq!(reference.url[0], reference_url);
} else {
assert!(false);
}
// producttree
let producttree_productid = "openEuler-22.03-LTS";
let producttree_cep = "cpe:/a:openEuler:openEuler:22.03-LTS";
let producttree_content = "openEuler-22.03-LTS";
assert_eq!(cvrf.producttree.products.len(), 6);
assert_eq!(
cvrf.producttree.products[2].productid,
producttree_productid
);
assert_eq!(cvrf.producttree.products[2].cpe, producttree_cep);
assert_eq!(cvrf.producttree.products[2].content, producttree_content);
let producttree_src = "src";
let producttree_src_productid = "golang-1.17.3-32";
let producttree_src_cep = "cpe:/a:openEuler:openEuler:22.03-LTS";
let producttree_src_content = "golang-1.17.3-32.oe2203.src.rpm";
assert_eq!(cvrf.producttree.packages.len(), 4);
assert_eq!(
cvrf.producttree.packages.get(producttree_src).unwrap()[2].productid,
producttree_src_productid
);
assert_eq!(
cvrf.producttree.packages.get(producttree_src).unwrap()[2].cpe,
producttree_src_cep
);
assert_eq!(
cvrf.producttree.packages.get(producttree_src).unwrap()[2].content,
producttree_src_content
);
// vulnerabilities
let cvrf_vulner_releasedate = "2024-04-19";
let cvrf_vulner_cve = "CVE-2023-45288";
let cvrf_vulner_productstatues_status = "Fixed";
let cvrf_vulner_productstatues_product = "openEuler-22.03-LTS";
let cvrf_vulner_threat = Severity::Important;
let cvrf_vulner_basescore = "7.5";
let cvrf_vulner_vector = "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H";
let cvrf_vulner_remedition_type = "Vendor Fix";
let cvrf_vulner_remedition_descrition = "golang security update";
let cvrf_vulner_remedition_date = "2024-04-19";
let cvrf_vulner_remedition_url = "https://www.openeuler.org/en/security/safety-bulletin/detail.html?id=openEuler-SA-2024-1488";
assert_eq!(cvrf.vulnerabilities[0].notes.len(), 1);
assert_eq!(cvrf.vulnerabilities[0].releasedate, cvrf_vulner_releasedate);
assert_eq!(cvrf.vulnerabilities[0].cve, cvrf_vulner_cve);
assert_eq!(
cvrf.vulnerabilities[0].productstatuses[0].status,
cvrf_vulner_productstatues_status
);
assert_eq!(
cvrf.vulnerabilities[0].productstatuses[0].products[2],
cvrf_vulner_productstatues_product
);
assert_eq!(cvrf.vulnerabilities[0].threats[0].description, cvrf_vulner_threat);
assert_eq!(
cvrf.vulnerabilities[0].cvssscoresets[0].basescore,
cvrf_vulner_basescore
);
assert_eq!(
cvrf.vulnerabilities[0].cvssscoresets[0].vector,
cvrf_vulner_vector
);
assert_eq!(
cvrf.vulnerabilities[0].remediations[0].r#type,
cvrf_vulner_remedition_type
);
assert_eq!(
cvrf.vulnerabilities[0].remediations[0].description,
cvrf_vulner_remedition_descrition
);
assert_eq!(
cvrf.vulnerabilities[0].remediations[0].date,
cvrf_vulner_remedition_date
);
assert_eq!(
cvrf.vulnerabilities[0].remediations[0].url,
cvrf_vulner_remedition_url
);
// SaInfo
let sa = cvrf.sainfo();
assert_eq!(sa.id, id);
}