1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Security advisories in the RustSec database

pub mod affected;
pub mod category;
pub mod date;
pub mod id;
pub mod informational;
pub mod keyword;
pub mod linter;
pub mod metadata;
pub mod versions;

pub use self::{
    affected::Affected, category::Category, date::Date, id::Id, informational::Informational,
    keyword::Keyword, linter::Linter, metadata::Metadata, versions::Versions,
};
pub use cvss::Severity;

use crate::error::{Error, ErrorKind};
use serde::{Deserialize, Serialize};
use std::{fs, path::Path, str::FromStr};

/// RustSec Security Advisories
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Advisory {
    /// The `[advisory]` section of a RustSec advisory
    #[serde(rename = "advisory")]
    pub metadata: Metadata,

    /// The (optional) `[affected]` section of a RustSec advisory
    pub affected: Option<Affected>,

    /// Versions related to this advisory which are patched or unaffected.
    pub versions: Versions,
}

impl Advisory {
    /// Load an advisory from a `RUSTSEC-20XX-NNNN.toml` file
    pub fn load_file(path: impl AsRef<Path>) -> Result<Self, Error> {
        let path = path.as_ref();

        // TODO(tarcieri): deprecate and remove legacy TOML-based advisory format
        match path.extension().and_then(|ext| ext.to_str()) {
            Some("toml") => {
                // Legacy TOML-based advisory format
                fs::read_to_string(path)
                    .map_err(|e| {
                        format_err!(ErrorKind::Io, "couldn't open {}: {}", path.display(), e)
                    })?
                    .parse()
            }
            Some("md") => {
                // New V3 Markdown-based advisory format
                Self::parse_v3(path)
            }
            _ => fail!(
                ErrorKind::Repo,
                "unexpected file extension: {}",
                path.display()
            ),
        }
    }

    /// Parse a V3 advisory from a string
    pub fn parse_v3(path: &Path) -> Result<Self, Error> {
        let advisory_data = fs::read_to_string(path)
            .map_err(|e| format_err!(ErrorKind::Io, "couldn't open {}: {}", path.display(), e))?;

        if !advisory_data.starts_with("```toml") {
            fail!(
                ErrorKind::Parse,
                "unexpected start of V3 advisory: {}",
                path.display()
            )
        }

        let toml_end = advisory_data.find("\n```").ok_or_else(|| {
            format_err!(
                ErrorKind::Parse,
                "couldn't find end of TOML front matter in advisory: {}",
                path.display()
            )
        })?;

        let front_matter = advisory_data[7..toml_end].trim_start().trim_end();
        let mut advisory: Self = toml::from_str(front_matter)?;

        if advisory.metadata.title != "" || advisory.metadata.description != "" {
            fail!(
                ErrorKind::Parse,
                "Markdown advisories MUST have empty title/description: {}",
                path.display()
            )
        }

        let markdown = advisory_data[(toml_end + 4)..].trim_start();

        if !markdown.starts_with("# ") {
            fail!(
                ErrorKind::Parse,
                "Expected # header after TOML front matter in: {}",
                path.display()
            );
        }

        let next_newline = markdown.find('\n').ok_or_else(|| {
            format_err!(
                ErrorKind::Parse,
                "no Markdown body (i.e. description) found: {}",
                path.display()
            )
        })?;

        advisory.metadata.title = markdown[2..next_newline].trim_end().to_owned();
        advisory.metadata.description = markdown[(next_newline + 1)..]
            .trim_start()
            .trim_end()
            .to_owned();

        Ok(advisory)
    }

    /// Get the severity of this advisory if it has a CVSS v3 associated
    pub fn severity(&self) -> Option<Severity> {
        self.metadata.cvss.as_ref().map(|cvss| cvss.severity())
    }
}

impl FromStr for Advisory {
    type Err = Error;

    fn from_str(toml_string: &str) -> Result<Self, Error> {
        let advisory: Self = toml::from_str(toml_string)?;

        if advisory.metadata.title == "" || advisory.metadata.description == "" {
            fail!(
                ErrorKind::Parse,
                "missing title and/or description in advisory:\n\n{}",
                toml_string
            )
        }

        Ok(advisory)
    }
}