use super::*;
use crate::api::security::{
Alert, AlertKind, AlertList, AlertState, Detail, SecretLocation, Update,
};
use std::fmt::Write as _;
pub fn security_nav(page: &mut Page, repo: &RepoId, on: Option<AlertKind>) {
let base = format!("{}/security", url::repo(repo));
let advisories = ("Advisories", format!("{base}/advisories"), on.is_none());
let kinds = AlertKind::ALL.into_iter().map(|kind| {
(
kind.name(),
format!("{base}/{}", kind.slug()),
on == Some(kind),
)
});
let tabs = std::iter::once(advisories).chain(kinds);
let segs = nav_strip(page, &meta(" · "), &Role::Link, tabs);
page.wrapped(segs, 0, Frame::None);
page.blank();
}
fn severity(a: &Alert) -> Option<Seg> {
match (a.severity, &a.level) {
(Some(s), _) => Some(super::project::severity_chip(s)),
(None, Some(level)) => {
let bg = match level.as_str() {
"error" => Bg::ErrorContainer,
"warning" => Bg::TertiaryContainer,
_ => Bg::SecondaryContainer,
};
Some(chip(capitalized(level), bg))
}
(None, None) => None,
}
}
fn state_chip(state: AlertState) -> Seg {
let bg = match state {
AlertState::Open => Bg::SuccessContainer,
AlertState::Fixed => Bg::PrimaryContainer,
_ => Bg::SecondaryContainer,
};
chip(state.label(), bg)
}
fn file_link(page: &mut Page, repo: &RepoId, path: &str, lines: (u32, u32), at: &str) -> Seg {
let (start, end) = lines;
let anchor = if start == end {
format!("#L{start}")
} else {
format!("#L{start}-L{end}")
};
let target = format!("{}{anchor}", url::blob(repo, at, path));
link_seg(page, format!("{path}:{start}"), target, Role::Link)
}
fn row_meta(a: &Alert, now: u64) -> String {
let mut subline = format!("#{}", a.number);
match &a.detail {
Detail::Dependency(d) => {
let _ = write!(subline, " · {} ({})", d.package, d.ecosystem);
if !d.manifest.is_empty() {
let _ = write!(subline, " · {}", d.manifest);
}
}
Detail::Code(c) => {
if let (Some(path), Some((line, _))) = (&c.path, c.lines) {
let _ = write!(subline, " · {path}:{line}");
}
if !c.tool.is_empty() {
let _ = write!(subline, " · {}", c.tool);
}
}
Detail::Secret(s) => {
if let Some(SecretLocation::File { path, lines, .. }) = s.locations.first() {
let _ = write!(subline, " · {path}:{}", lines.0);
}
}
}
let when = match (&a.closing, &a.fixed_at) {
(Some(c), _) => {
c.at.as_deref()
.map(|at| format!("closed {}", time::ago_iso(at, now)))
}
(None, Some(at)) => Some(format!("fixed {}", time::ago_iso(at, now))),
(None, None) => Some(format!("opened {}", time::ago_iso(&a.created_at, now))),
};
if let Some(when) = when {
let _ = write!(subline, " · {when}");
}
subline
}
pub fn alerts(
page: &mut Page,
repo: &RepoId,
kind: AlertKind,
query: &str,
list: Fetched<'_, AlertList>,
cx: PageCtx<'_>,
) {
let PageCtx { keys, now, .. } = cx;
filter_field(page, query, keys, "filter");
let state = list_state(query);
let tabs = [("open", "Open"), ("closed", "Closed")]
.map(|(name, label)| (label, Link::State(name.to_owned()), state == name));
let title = nav_strip(page, &Seg::new(" ", Role::Body), &Role::Meta, tabs);
let alerts = kind.alerts().to_lowercase();
let loading = |page: &mut Page| loading_box(page, title.clone());
let Some(l) = list.show_or(page, &alerts, loading) else { return };
let ignored = crate::api::security::filter(kind, query).ignored;
if !ignored.is_empty() {
let note = format!("Not a filter of {alerts}: {}", ignored.join(" "));
say(page, note, Role::Meta);
page.blank();
}
if let Some(off) = &l.off {
let text = format!("{off}.");
page.box_top(title, Vec::new());
empty_row(page, &text);
page.box_bottom();
return;
}
let empty = match state {
"open" => "No open alerts. 🎉",
_ => "No alerts matched the filter.",
};
list_box(page, title, Vec::new(), &l.results, empty, |page, a| {
let target = format!("{}/security/{}/{}", url::repo(repo), kind.slug(), a.number);
item(page, target, |page, link| {
let mut segs = Vec::new();
if let Some(chip) = severity(a) {
segs.push(chip);
segs.push(space());
}
segs.push(Seg::linked(a.title.clone(), Role::Strong, link));
let right = if a.state.is_open() {
Vec::new()
} else {
vec![state_chip(a.state)]
};
page.box_line(segs, right, 0);
body(page, vec![meta(row_meta(a, now))]);
});
});
}
pub fn alert(page: &mut Page, repo: &RepoId, a: &Alert, cx: PageCtx<'_>) {
let now = cx.now;
say(page, a.title.clone(), Role::Title);
let mut segs = vec![state_chip(a.state)];
if let Some(chip) = severity(a) {
segs.push(space());
segs.push(chip);
}
let mut subline = format!(
" {} alert #{} · opened {}",
a.kind().name(),
a.number,
time::ago_iso(&a.created_at, now)
);
if let Some(at) = &a.fixed_at {
let _ = write!(subline, " · fixed {}", time::ago_iso(at, now));
}
segs.push(meta(subline));
page.wrapped(segs, 0, Frame::None);
if let Some(c) = &a.closing {
let mut segs = vec![strong("Closed")];
if let Some(by) = &c.by {
segs.push(meta(" by "));
segs.push(login_seg(page, by, Role::Link));
}
if let Some(at) = &c.at {
segs.push(meta(format!(" {}", time::ago_iso(at, now))));
}
if let Some(reason) = a.reason_label() {
segs.push(meta(format!(" · {reason}")));
}
page.wrapped(segs, 0, Frame::None);
if let Some(comment) = &c.comment {
page.wrapped(vec![Seg::new(comment.clone(), Role::Body)], 2, Frame::None);
}
}
page.blank();
match &a.detail {
Detail::Dependency(d) => dependency(page, repo, d),
Detail::Code(c) => code(page, repo, c),
Detail::Secret(s) => secret(page, repo, s),
}
}
fn dependency(page: &mut Page, repo: &RepoId, d: &crate::api::security::Dependency) {
let title = vec![strong(format!("{} ({})", d.package, d.ecosystem))];
page.box_top(title, Vec::new());
let facts = affected(d.vulnerable.as_ref(), d.patched.as_deref());
body(page, vec![Seg::new(facts, Role::Body)]);
let mut where_ = Vec::new();
if !d.manifest.is_empty() {
let target = url::blob(repo, "HEAD", &d.manifest);
where_.push(link_seg(page, d.manifest.clone(), target, Role::Link));
}
let about: Vec<&str> = [d.scope.as_deref(), d.relationship.as_deref()]
.into_iter()
.flatten()
.collect();
if !about.is_empty() {
where_.push(meta(format!(" · {}", about.join(", "))));
}
if !where_.is_empty() {
body(page, where_);
}
match &d.update {
Some(Update::Pr {
number,
title,
state,
}) => {
let pr = PrRef {
repo: repo.clone(),
number: *number,
};
let state = state.to_lowercase();
let (text, pr) = (format!("#{number} {title}"), url::pull(&pr));
let link = link_seg(page, text, pr, Role::Link);
let state = meta(format!(" · {state}"));
body(page, vec![meta("Dependabot's pull request "), link, state]);
}
Some(Update::Failed { title, body: why }) => {
let why_not = format!("Dependabot couldn't open a pull request: {title}");
body(page, vec![meta(why_not)]);
if let Some(first) = why.lines().find(|l| !l.trim().is_empty()) {
body(page, vec![meta(first.trim().to_owned())]);
}
}
None => {}
}
page.box_bottom();
page.blank();
let advisory = format!("{}/advisories/{}", url::base(), d.ghsa);
let mut ids = vec![link_seg(page, &d.ghsa, advisory, Role::Link)];
if let Some(cve) = &d.cve {
ids.push(meta(format!(" · {cve}")));
}
if !d.cwes.is_empty() {
ids.push(meta(format!(" · {}", d.cwes.join(" · "))));
}
page.wrapped(ids, 0, Frame::None);
if !d.description.trim().is_empty() {
page.blank();
markdown::render(page, &d.description, None, Frame::None);
}
references(page, &d.references);
}
fn code(page: &mut Page, repo: &RepoId, c: &crate::api::security::CodeAlert) {
let mut title = vec![strong(c.rule.clone())];
if !c.tool.is_empty() {
title.push(meta(format!(" {}", c.tool)));
}
page.box_top(title, Vec::new());
if !c.message.is_empty() {
page.box_line(vec![Seg::new(c.message.clone(), Role::Body)], Vec::new(), 0);
}
match (&c.path, c.lines, &c.commit) {
(Some(path), Some(lines), Some(at)) => {
let mut segs = vec![file_link(page, repo, path, lines, at)];
let on = c
.git_ref
.as_deref()
.map(|r| r.strip_prefix("refs/heads/").unwrap_or(r))
.map_or_else(String::new, |r| format!(" on {r}"));
segs.push(meta(format!("{on} at {}", short_sha(at))));
body(page, segs);
}
(Some(path), ..) => body(page, vec![meta(path.clone())]),
_ => {}
}
if !c.tags.is_empty() {
body(page, vec![meta(c.tags.join(" · "))]);
}
page.box_bottom();
if !c.rule_description.is_empty() {
page.blank();
say(page, c.rule_description.clone(), Role::Body);
}
if !c.help.trim().is_empty() {
page.blank();
markdown::render(page, &c.help, None, Frame::None);
}
}
fn secret(page: &mut Page, repo: &RepoId, s: &crate::api::security::SecretAlert) {
let title = vec![strong(s.secret_type.clone())];
let right = match s.validity.as_deref() {
Some("active") => vec![chip("Active", Bg::ErrorContainer)],
Some("inactive") => vec![chip("Inactive", Bg::SecondaryContainer)],
_ => Vec::new(),
};
page.box_top(title, right);
let mut facts = Vec::new();
if s.publicly_leaked {
facts.push("Publicly leaked".to_owned());
}
if let Some(by) = &s.bypassed_by {
facts.push(format!("Pushed past push protection by @{by}"));
}
if !facts.is_empty() {
body(page, vec![meta(facts.join(" · "))]);
}
if s.locations.is_empty() {
empty_row(page, "No locations listed.");
}
for l in &s.locations {
match l {
SecretLocation::File {
path,
lines,
commit,
} => {
let segs = vec![
file_link(page, repo, path, *lines, commit),
meta(format!(" at {}", short_sha(commit))),
];
body(page, segs);
}
SecretLocation::Other(what) => {
body(page, vec![meta(format!("Found in: {what}"))]);
}
}
}
page.box_bottom();
page.blank();
let note = "The secret itself isn't shown: revoke it where it was issued, then close the alert as revoked.";
say(page, note, Role::Meta);
}