Skip to content

Commit

Permalink
use two modules and TryFrom<>
Browse files Browse the repository at this point in the history
  • Loading branch information
meghfossa committed Dec 12, 2023
1 parent 4164ac2 commit 6adc7d8
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 205 deletions.
221 changes: 221 additions & 0 deletions src/ingestion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::Path;

use anyhow::{Context, Result};
use chrono::Utc;
use clap::{crate_name, crate_version};
use indoc::{formatdoc, indoc};
use locator::Locator;
use non_empty_string::NonEmptyString;
use serde::de::Error;
use serde::{Deserialize, Serialize};
use tap::TapFallible;

use crate::migration::MigrationVulnComponentEntry;

#[derive(Debug, Serialize, Deserialize)]
pub struct VulnComponentBatch {
pub entries: Vec<VulnComponentEntry>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VulnComponentEntry {
pub cve: NonEmptyString,
pub dependency_revision_id: Locator,
pub function: SymbolTarget,
pub researcher: NonEmptyString,
pub evidence_notes: Option<String>,
pub file_path: Option<String>,
pub line_start: Option<u32>,
}

impl TryFrom<VulnComponentEntry> for MigrationVulnComponentEntry {
type Error = serde_json::Error;
fn try_from(v: VulnComponentEntry) -> Result<Self, Self::Error> {
Ok(Self {
cve: v.cve,
dependency_revision_id: v.dependency_revision_id,
function: serde_json::to_string(&v.function)?,
researcher: v.researcher,
evidence_notes: v.evidence_notes,
file_path: v.file_path,
line_start: v.line_start,
})
}
}

#[derive(Debug, Serialize, PartialEq)]
pub struct JavaSymbols(Vec<SymbolJava>);

// TODO: This should be using same type as reachability lib
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SymbolTarget {
Java { symbol: JavaSymbols },
}

// TODO: This should be using same type as reachability lib
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum SymbolJava {
Package { label: String },
Class { label: String },
ClassMethod { label: String },
Constructor { label: String },
}

impl<'de> Deserialize<'de> for JavaSymbols {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if !s.is_empty() {
let symbols: Vec<SymbolJava> = s.split("::").filter_map(parse_symbol_java).collect();
if !symbols.is_empty() {
return Ok(JavaSymbols(symbols));
}
}

Err(Error::invalid_value(
serde::de::Unexpected::Str(s.as_str()),
&"fully qualified symbol path: Class(SomeClassName)::ClassMethod(SomeClassMethod)",
))
}
}

impl VulnComponentBatch {
pub fn make_example(out_file: &Path) -> Result<()> {
let content = indoc! {"
entries:
- cve: 'CVE-2022-37865' # ID of the CVE
dependency_revision_id: 'mvn+org.apache.ivy:ivy' # FOSSA locator: <ecosystem>+<package>$<version>
function: # Vulnerable Function
kind: java # Kind of function. Possible choices: 'java'.
symbol: 'Class(ZipPacking)::ClassMethod(unpack)' # Qualified Function Path. Possible choices: Class(), ClassMethod(), Constructor(). `::` denotes sub scope.
researcher: 'Name of the person' # Name of the researcher
notes: 'some notes' # Any notes (Optional)
"};

File::create(out_file)
.context("creating file")?
.write_all(content.as_bytes())
.context("writing file")
.tap_ok(|_| println!("Wrote file at: {:?}", out_file))
}

pub fn make_migration(batch_file: &Path) -> Result<()> {
// 1. Read file
let file =
File::open(batch_file).with_context(|| format!("open file `{:?}`", batch_file))?;
let mut content = String::new();
let mut reader = io::BufReader::new(file);

reader
.read_to_string(&mut content)
.with_context(|| format!("read content of: `{:?}`", batch_file))?;

let batch: VulnComponentBatch =
serde_yaml::from_str(&content).with_context(|| format!("parse: `{:?}`", batch_file))?;
let entries = batch
.entries
.into_iter()
.map(MigrationVulnComponentEntry::try_from)
.collect::<Result<Vec<_>, _>>()
.context("convert to output format")?;

// 2. Create migration file's content
let entries: String =
serde_json::to_string_pretty(&entries).context("serialize entries")?;
let content = formatdoc! {"
// Automatically generated by: {app_name} {app_version}
/* eslint-disable quotes */
/* eslint-disable quote-props */
/* eslint-disable comma-dangle */
const entries = {entries};
module.exports = {{
up: async (queryInterface, _sequelize) => {{
queryInterface.bulkInsert('ReachabilityPrivateVulnComponents', entries, {{
updateOnDuplicate: ['evidence_notes', 'file_path', 'line_start', 'researcher', 'updated_at'],
upsertKeys: ['cve', 'dependency_revision_id', 'function'],
}});
}},
down: async (_queryInterface, _) => {{}},
}};
",
app_name = crate_name!(),
app_version = crate_version!(),
};

// 3. Write migration file!
let out_file = &format!(
"{}-vulnComponent-automated.js",
Utc::now().format("%Y%m%d%H%M%S")
);
File::create(out_file)
.context("creating file")?
.write_all(content.as_bytes())
.context("writing file")
.tap_ok(|_| println!("Wrote file at: {:?}", out_file))
}
}

fn parse_symbol_java(input: &str) -> Option<SymbolJava> {
let mut parts = input.split("::");
let kind_label_pair = parts.next()?;
let Some((kind, label)) = kind_label_pair.split_once('(') else {
return None;
};

let kind = kind.trim();
let label = label.trim_end_matches(')');

match kind {
"Package" => Some(SymbolJava::Package {
label: label.to_string(),
}),
"Class" => Some(SymbolJava::Class {
label: label.to_string(),
}),
"ClassMethod" => Some(SymbolJava::ClassMethod {
label: label.to_string(),
}),
"Constructor" => Some(SymbolJava::Constructor {
label: label.to_string(),
}),
_ => None,
}
}

#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;

#[
test_case("Class(Logger)::ClassMethod(log)",
vec![
SymbolJava::Class {label: "Logger".to_string() },
SymbolJava::ClassMethod { label: "log".to_string() }
]
)]
#[
test_case("Class(Logger)",
vec![
SymbolJava::Class {label: "Logger".to_string() },
]
)]
fn parse_java_symbols_works(arg: &str, expected: Vec<SymbolJava>) {
let json_string = format!(r#"{{ "kind": "java", "symbol": "{arg}" }}"#);
let symbol: SymbolTarget =
serde_json::from_str(&json_string).expect("to serialize into json string");
assert_eq!(
symbol,
SymbolTarget::Java {
symbol: JavaSymbols(expected)
}
)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod ingestion;
pub mod migration;
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use clap::{Parser, Subcommand};
use reachability_toolkit::migration::{Migrate, VulnComponentBatch};
use reachability_toolkit::ingestion::VulnComponentBatch;
use std::path::PathBuf;

#[derive(Parser, Debug)]
Expand Down
Loading

0 comments on commit 6adc7d8

Please sign in to comment.