Skip to main content

solid_pod_rs/
metrics.rs

1//! Minimal metrics sink for security primitives.
2//!
3//! A zero-dependency, lock-free counter bundle for the Sprint 4 F1/F2
4//! security aggregates. Prometheus export is intentionally out of
5//! scope: the upstream binder crate (`webxr`) already runs a
6//! Prometheus registry and can lift these atomics into gauges when it
7//! wires the primitives in Sprint 4 / F7.
8//!
9//! The struct is `Clone` (cheap — a handful of `Arc<AtomicU64>`), so
10//! a single instance can be cloned into both `SsrfPolicy` and
11//! `DotfileAllowlist` via their `with_metrics(_)` builders and also
12//! retained by the operator for scraping.
13
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::Arc;
16
17// `IpClass` lives in the SSRF guard which only compiles under
18// `tokio-runtime` (DNS resolution requires the tokio reactor). The
19// SSRF-block counter helpers below are gated to match. Dotfile
20// counters remain available under `core`.
21#[cfg(feature = "tokio-runtime")]
22use crate::security::ssrf::IpClass;
23
24/// Atomic counter bundle, cheap to clone.
25#[derive(Debug, Default, Clone)]
26pub struct SecurityMetrics {
27    inner: Arc<SecurityMetricsInner>,
28}
29
30#[derive(Debug, Default)]
31struct SecurityMetricsInner {
32    // SSRF block counters, labelled by IpClass. Only read by the
33    // SSRF-block helpers, which are gated on `tokio-runtime`. The
34    // fields stay in the struct unconditionally so the layout — and
35    // therefore `Default`/`Clone` derivations — is identical across
36    // feature configurations.
37    #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))]
38    ssrf_blocked_private: AtomicU64,
39    #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))]
40    ssrf_blocked_loopback: AtomicU64,
41    #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))]
42    ssrf_blocked_link_local: AtomicU64,
43    #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))]
44    ssrf_blocked_multicast: AtomicU64,
45    #[cfg_attr(not(feature = "tokio-runtime"), allow(dead_code))]
46    ssrf_blocked_reserved: AtomicU64,
47    // `Public` is never blocked under the default classifier, but
48    // callers that carry a denylist hit count it under `Reserved`
49    // (denylist is operator-explicit intent).
50
51    // Dotfile deny counter.
52    dotfile_denied: AtomicU64,
53}
54
55impl SecurityMetrics {
56    /// Construct a fresh counter bundle. All counters start at zero.
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Increment the SSRF block counter for `class`.
62    #[cfg(feature = "tokio-runtime")]
63    pub fn record_ssrf_block(&self, class: IpClass) {
64        let counter = match class {
65            IpClass::Private => &self.inner.ssrf_blocked_private,
66            IpClass::Loopback => &self.inner.ssrf_blocked_loopback,
67            IpClass::LinkLocal => &self.inner.ssrf_blocked_link_local,
68            IpClass::Multicast => &self.inner.ssrf_blocked_multicast,
69            IpClass::Reserved | IpClass::Public => &self.inner.ssrf_blocked_reserved,
70        };
71        counter.fetch_add(1, Ordering::Relaxed);
72    }
73
74    /// Read the SSRF block counter for `class`.
75    #[cfg(feature = "tokio-runtime")]
76    pub fn ssrf_blocked_total(&self, class: IpClass) -> u64 {
77        let counter = match class {
78            IpClass::Private => &self.inner.ssrf_blocked_private,
79            IpClass::Loopback => &self.inner.ssrf_blocked_loopback,
80            IpClass::LinkLocal => &self.inner.ssrf_blocked_link_local,
81            IpClass::Multicast => &self.inner.ssrf_blocked_multicast,
82            IpClass::Reserved | IpClass::Public => &self.inner.ssrf_blocked_reserved,
83        };
84        counter.load(Ordering::Relaxed)
85    }
86
87    /// Increment the dotfile-deny counter.
88    pub fn record_dotfile_deny(&self) {
89        self.inner.dotfile_denied.fetch_add(1, Ordering::Relaxed);
90    }
91
92    /// Read the dotfile-deny counter.
93    pub fn dotfile_denied_total(&self) -> u64 {
94        self.inner.dotfile_denied.load(Ordering::Relaxed)
95    }
96}