use crate::RepoPath;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use time::OffsetDateTime;
pub const SCHEMA_REPORT_V1: &str = "depguard.report.v1";
pub const SCHEMA_REPORT_V2: &str = "depguard.report.v2";
pub const SCHEMA_SENSOR_REPORT_V1: &str = "sensor.report.v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Warning,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Location {
pub path: RepoPath,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub col: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Finding {
pub severity: Severity,
pub check_id: String,
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<Location>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub data: JsonValue,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Verdict {
Pass,
Warn,
Fail,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ToolMeta {
pub name: String,
pub version: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SeverityV2 {
Info,
Warn,
Error,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum VerdictStatus {
Pass,
Warn,
Fail,
Skip,
}
fn is_zero(val: &u32) -> bool {
*val == 0
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct VerdictCounts {
pub info: u32,
pub warn: u32,
pub error: u32,
#[serde(default, skip_serializing_if = "is_zero")]
pub suppressed: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct VerdictV2 {
pub status: VerdictStatus,
pub counts: VerdictCounts,
#[serde(default)]
pub reasons: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ToolMetaV2 {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RunHost {
#[serde(skip_serializing_if = "Option::is_none")]
pub os: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RunCi {
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub job: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RunGit {
#[serde(skip_serializing_if = "Option::is_none")]
pub repo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_sha: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head_sha: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub merge_base: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum CapabilityAvailability {
Available,
Missing,
Degraded,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CapabilityStatus {
pub status: CapabilityAvailability,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Capabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub git: Option<CapabilityStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<CapabilityStatus>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct RunMeta {
#[schemars(with = "String")]
#[serde(with = "time::serde::rfc3339")]
pub started_at: OffsetDateTime,
#[schemars(with = "Option<String>")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "time::serde::rfc3339::option")]
pub ended_at: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<RunHost>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ci: Option<RunCi>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git: Option<RunGit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Capabilities>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct FindingV2 {
pub severity: SeverityV2,
pub check_id: String,
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<Location>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub data: JsonValue,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ArtifactType {
Comment,
Annotation,
Extra,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ArtifactPointer {
#[serde(rename = "type")]
pub artifact_type: ArtifactType,
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
pub struct DepguardData {
pub scope: String,
pub profile: String,
pub manifests_scanned: u32,
pub dependencies_scanned: u32,
pub findings_total: u32,
pub findings_emitted: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncated_reason: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ReportEnvelope<TData = DepguardData> {
pub schema: String,
pub tool: ToolMeta,
#[schemars(with = "String")]
#[serde(with = "time::serde::rfc3339")]
pub started_at: OffsetDateTime,
#[schemars(with = "String")]
#[serde(with = "time::serde::rfc3339")]
pub finished_at: OffsetDateTime,
pub verdict: Verdict,
pub findings: Vec<Finding>,
pub data: TData,
}
pub type DepguardReportV1 = ReportEnvelope<DepguardData>;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ReportEnvelopeV2<TData = DepguardData> {
pub schema: String,
pub tool: ToolMetaV2,
pub run: RunMeta,
pub verdict: VerdictV2,
pub findings: Vec<FindingV2>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artifacts: Option<Vec<ArtifactPointer>>,
pub data: TData,
}
pub type DepguardReportV2 = ReportEnvelopeV2<DepguardData>;
pub type DepguardReport = DepguardReportV1;
#[cfg(test)]
mod tests {
use super::*;
use time::OffsetDateTime;
#[test]
fn verdict_counts_skip_zero_suppressed() {
let counts = VerdictCounts {
info: 0,
warn: 0,
error: 0,
suppressed: 0,
};
let value = serde_json::to_value(&counts).unwrap();
assert!(value.get("suppressed").is_none());
let counts = VerdictCounts {
info: 0,
warn: 1,
error: 0,
suppressed: 2,
};
let value = serde_json::to_value(&counts).unwrap();
assert_eq!(value["suppressed"], 2);
}
#[test]
fn run_meta_omits_optional_fields() {
let run = RunMeta {
started_at: OffsetDateTime::UNIX_EPOCH,
ended_at: None,
duration_ms: None,
host: None,
ci: None,
git: None,
capabilities: None,
};
let value = serde_json::to_value(&run).unwrap();
assert!(value.get("ended_at").is_none());
assert!(value.get("duration_ms").is_none());
assert!(value.get("host").is_none());
assert!(value.get("ci").is_none());
assert!(value.get("git").is_none());
assert!(value.get("capabilities").is_none());
assert!(value.get("started_at").is_some());
}
}