mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 05:19:35 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dc9c84b29 |
No files matched your search
@@ -18,7 +18,7 @@ pub struct FirewallConfig {
|
||||
pub min_age_hours: u64,
|
||||
pub denylist: DenylistSource,
|
||||
/// Packages never screened, by ecosystem: `{"npm": [{"name": "left-pad"}]}`.
|
||||
pub allow: HashMap<String, Vec<AllowedPackage>>,
|
||||
pub allow: PackageList,
|
||||
pub telemetry: bool,
|
||||
/// Reports installs to, and takes blocks from, a Strix workspace.
|
||||
pub platform: Option<PlatformConfig>,
|
||||
@@ -27,9 +27,12 @@ pub struct FirewallConfig {
|
||||
pub registries: Vec<&'static Registry>,
|
||||
}
|
||||
|
||||
/// Packages by ecosystem, as the allow list and organization blocks name them.
|
||||
pub type PackageList = HashMap<String, Vec<ListedPackage>>;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AllowedPackage {
|
||||
pub struct ListedPackage {
|
||||
pub name: String,
|
||||
/// Every version when absent.
|
||||
pub version: Option<String>,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::config::FirewallConfig;
|
||||
use crate::policies::filters::age::Age;
|
||||
use crate::policies::filters::denylist::Denylist;
|
||||
use crate::policies::filters::organization::OrganizationBlocks;
|
||||
use crate::policies::filters::osv::Osv;
|
||||
use crate::policies::filters::Filter;
|
||||
use crate::policies::registries::*;
|
||||
@@ -28,7 +29,12 @@ impl Firewall {
|
||||
}
|
||||
let ecosystems = config.registries.iter().map(|r| r.osv_name);
|
||||
let denylist = Denylist::new(config.denylist.clone(), ecosystems);
|
||||
let mut filters: Vec<Box<dyn Filter>> = vec![Box::new(denylist), Box::new(Osv)];
|
||||
let mut filters: Vec<Box<dyn Filter>> = Vec::new();
|
||||
if let Some(platform) = &config.platform {
|
||||
filters.push(Box::new(OrganizationBlocks::new(platform.clone())));
|
||||
}
|
||||
filters.push(Box::new(denylist));
|
||||
filters.push(Box::new(Osv));
|
||||
if config.min_age_hours > 0 {
|
||||
filters.push(Box::new(Age::new(config.min_age_hours)));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
pub mod age;
|
||||
pub mod denylist;
|
||||
pub mod organization;
|
||||
pub mod osv;
|
||||
|
||||
use super::registries::{Package, Registry};
|
||||
use crate::config::PackageList;
|
||||
use crate::Firewall;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
@@ -30,7 +32,7 @@ pub async fn consult(
|
||||
registry: &'static Registry,
|
||||
pkg: &Package,
|
||||
) -> Option<(&'static str, Verdict)> {
|
||||
if allowed(firewall, registry, pkg) {
|
||||
if listed(&firewall.config.allow, registry, pkg) {
|
||||
return None;
|
||||
}
|
||||
let mut unsure = None;
|
||||
@@ -45,8 +47,9 @@ pub async fn consult(
|
||||
unsure
|
||||
}
|
||||
|
||||
fn allowed(firewall: &Firewall, registry: &Registry, pkg: &Package) -> bool {
|
||||
let Some(entries) = firewall.config.allow.get(registry.eco) else {
|
||||
/// Whether `list` names `pkg`.
|
||||
fn listed(list: &PackageList, registry: &Registry, pkg: &Package) -> bool {
|
||||
let Some(entries) = list.get(registry.eco) else {
|
||||
return false;
|
||||
};
|
||||
entries.iter().any(|entry| {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
use super::{listed, Filter, Verdict, VerdictFuture, AGENT};
|
||||
use crate::config::{PackageList, PlatformConfig};
|
||||
use crate::policies::registries::Registry;
|
||||
use crate::policies::Package;
|
||||
use rama::http::client::EasyHttpWebClient;
|
||||
use rama::http::header::{AUTHORIZATION, USER_AGENT};
|
||||
use rama::http::service::client::HttpClientExt;
|
||||
use rama::http::{BodyExtractExt, StatusCode};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{info, warn};
|
||||
|
||||
const POLL: Duration = Duration::from_secs(60);
|
||||
|
||||
type Blocks = Arc<OnceCell<RwLock<Arc<Result<PackageList, String>>>>>;
|
||||
|
||||
/// Packages a Strix admin blocked, downloaded when the first package is screened.
|
||||
pub struct OrganizationBlocks {
|
||||
platform: PlatformConfig,
|
||||
blocks: Blocks,
|
||||
watch: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl OrganizationBlocks {
|
||||
pub fn new(platform: PlatformConfig) -> Self {
|
||||
let blocks = Arc::new(OnceCell::new());
|
||||
let watch = tokio::spawn(watch(platform.clone(), Arc::clone(&blocks)));
|
||||
OrganizationBlocks { platform, blocks, watch }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OrganizationBlocks {
|
||||
fn drop(&mut self) {
|
||||
self.watch.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for OrganizationBlocks {
|
||||
fn name(&self) -> &'static str {
|
||||
"organization"
|
||||
}
|
||||
|
||||
fn check<'a>(&'a self, registry: &'static Registry, pkg: &'a Package) -> VerdictFuture<'a> {
|
||||
Box::pin(async move {
|
||||
let load = || async { RwLock::new(Arc::new(load(&self.platform).await)) };
|
||||
let blocks = self.blocks.get_or_init(load).await;
|
||||
let blocks = Arc::clone(&blocks.read().expect("blocks lock"));
|
||||
match &*blocks {
|
||||
Err(e) => Verdict::Unknown(e.clone()),
|
||||
Ok(blocks) if listed(blocks, registry, pkg) => {
|
||||
Verdict::Deny("is blocked by your organization".into())
|
||||
}
|
||||
Ok(_) => Verdict::Allow,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A failed fetch leaves the last good list in place.
|
||||
async fn watch(platform: PlatformConfig, blocks: Blocks) {
|
||||
loop {
|
||||
tokio::time::sleep(POLL).await;
|
||||
let Some(current) = blocks.get() else { continue };
|
||||
if let Ok(fresh) = load(&platform).await {
|
||||
*current.write().expect("blocks lock") = Arc::new(Ok(fresh));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn load(platform: &PlatformConfig) -> Result<PackageList, String> {
|
||||
let blocks = fetch(platform).await;
|
||||
match &blocks {
|
||||
Ok(_) => info!("organization blocks loaded"),
|
||||
Err(error) => warn!(%error, "organization blocks unavailable"),
|
||||
}
|
||||
blocks
|
||||
}
|
||||
|
||||
async fn fetch(platform: &PlatformConfig) -> Result<PackageList, String> {
|
||||
let url = format!("{}/api/v1/firewall/blocklist", platform.url);
|
||||
let client = EasyHttpWebClient::default();
|
||||
let request = client
|
||||
.get(&url)
|
||||
.header(USER_AGENT, AGENT)
|
||||
.header(AUTHORIZATION, format!("Bearer {}", platform.api_key));
|
||||
let response = request.send().await.map_err(|e| e.to_string())?;
|
||||
if response.status() != StatusCode::OK {
|
||||
return Err(format!("{url} replied {}", response.status()));
|
||||
}
|
||||
let text = response.try_into_string().await.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(&text).map_err(|e| e.to_string())
|
||||
}
|
||||
Reference in new issue
Block a user