Skip to content

Commit 24702bd

Browse files
homanpcursoragent
andauthored
feat: add registry plugin system (#27)
* feat: implement generic registry plugin system Refactor package scanning into a pluggable adapter-based architecture: - Add RegistryAdapter trait for registry-specific logic - Create unified ExtractedPackage and PackageMetadata types - Implement NpmAdapter and PypiAdapter from existing code - Add AdapterRegistry for managing multiple adapters - Refactor PackageScanner to use scan_unified() and scan_tarball_unified() - Update capabilities and agentic modules for unified types - Remove old scanner/npm.rs and scanner/pypi.rs (moved to registry/) This enables easier addition of new registries (crates.io, etc.) by implementing the RegistryAdapter trait. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: bump version to 0.1.7 Co-authored-by: Cursor <cursoragent@cursor.com> * chore: update gitignore Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 89818ab commit 24702bd

File tree

12 files changed

+806
-639
lines changed

12 files changed

+806
-639
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ Cargo.lock
2525
# Logs
2626
*.log
2727

28-
awesome-express-starter
28+
AGENTS.md
29+
CLAUDE.md

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ members = [
1111
]
1212

1313
[workspace.package]
14-
version = "0.1.6"
14+
version = "0.1.7"
1515
edition = "2021"
1616
authors = ["sus contributors"]
1717
license = "MIT"

crates/common/src/models.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ pub enum ScanPriority {
9090
}
9191

9292
/// Package registry type
93-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type, Default)]
93+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type, Default)]
9494
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
9595
#[serde(rename_all = "lowercase")]
9696
pub enum Registry {

crates/worker/src/main.rs

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
//! sus Scan Worker - processes package scan jobs from the queue
22
3+
mod registry;
34
mod scanner;
45
mod skill_generator;
56

67
use anyhow::Result;
78
use axum::{routing::get, Json, Router};
8-
use common::{Database, Registry, ScanQueue};
9+
use common::{Database, ScanQueue};
910
use scanner::{AgenticScanner, PackageScanner};
1011
use std::net::SocketAddr;
1112
use std::time::Duration;
@@ -95,40 +96,17 @@ async fn worker_loop(queue: ScanQueue, scanner: PackageScanner) {
9596

9697
let start = std::time::Instant::now();
9798

98-
// Handle tarball jobs vs registry jobs based on registry type
99+
// Handle tarball jobs vs registry jobs using unified scan methods
99100
let scan_result = if let Some(tarball_path) = &job.tarball_path {
100-
// Local tarball - determine type based on registry
101-
match job.registry {
102-
Registry::Pypi => {
103-
scanner
104-
.scan_pypi_tarball(std::path::Path::new(tarball_path))
105-
.await
106-
}
107-
_ => {
108-
// Default to npm for tarballs
109-
scanner
110-
.scan_tarball(std::path::Path::new(tarball_path))
111-
.await
112-
}
113-
}
101+
// Local tarball - use unified tarball scan
102+
scanner
103+
.scan_tarball_unified(job.registry, std::path::Path::new(tarball_path))
104+
.await
114105
} else {
115-
// Remote registry scan
116-
match job.registry {
117-
Registry::Npm => scanner.scan(&job.package, job.version.as_deref()).await,
118-
Registry::Pypi => {
119-
scanner
120-
.scan_pypi(&job.package, job.version.as_deref())
121-
.await
122-
}
123-
Registry::Crates => {
124-
// TODO: Implement crates.io support
125-
tracing::warn!(
126-
package = %job.package,
127-
"Crates.io registry not yet supported"
128-
);
129-
Err(anyhow::anyhow!("Crates.io registry not yet supported"))
130-
}
131-
}
106+
// Remote registry scan - use unified scan
107+
scanner
108+
.scan_unified(job.registry, &job.package, job.version.as_deref())
109+
.await
132110
};
133111

134112
match scan_result {

crates/worker/src/registry/mod.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! Registry adapter module
2+
//!
3+
//! This module provides a unified interface for interacting with different package registries
4+
//! (npm, PyPI, etc.) through the `RegistryAdapter` trait.
5+
6+
mod npm;
7+
mod pypi;
8+
mod types;
9+
10+
pub use npm::NpmAdapter;
11+
pub use pypi::PypiAdapter;
12+
pub use types::{ExtractedPackage, Language, Maintainer, PackageMetadata, SourceFile};
13+
14+
use anyhow::Result;
15+
use async_trait::async_trait;
16+
use common::Registry;
17+
use std::collections::HashMap;
18+
use std::sync::Arc;
19+
20+
/// Trait for registry-specific operations
21+
///
22+
/// Each registry (npm, PyPI, etc.) implements this trait to provide
23+
/// a unified interface for fetching metadata, downloading packages,
24+
/// and computing trust scores.
25+
#[async_trait]
26+
pub trait RegistryAdapter: Send + Sync {
27+
/// Which registry this adapter handles
28+
fn registry(&self) -> Registry;
29+
30+
/// Fetch package metadata (version, maintainers, downloads, etc.)
31+
///
32+
/// If `version` is None, fetches the latest version.
33+
async fn fetch_metadata(&self, name: &str, version: Option<&str>) -> Result<PackageMetadata>;
34+
35+
/// Download and extract package to a temporary directory
36+
async fn download_package(&self, name: &str, version: &str) -> Result<ExtractedPackage>;
37+
38+
/// Extract a local tarball/package file
39+
fn extract_local(&self, path: &std::path::Path) -> Result<ExtractedPackage>;
40+
41+
/// Compute trust score (0-100) based on registry-specific factors
42+
fn compute_trust_score(&self, metadata: &PackageMetadata) -> u8;
43+
44+
/// Get CVE ecosystem identifier (e.g., "npm", "PyPI")
45+
///
46+
/// Returns None if this registry doesn't have CVE tracking.
47+
fn cve_ecosystem(&self) -> Option<&'static str>;
48+
49+
/// Fetch weekly download count (if available)
50+
async fn fetch_downloads(&self, name: &str) -> Result<Option<i64>>;
51+
}
52+
53+
/// Registry for managing all available adapters
54+
pub struct AdapterRegistry {
55+
adapters: HashMap<Registry, Arc<dyn RegistryAdapter>>,
56+
}
57+
58+
impl AdapterRegistry {
59+
/// Create a new adapter registry with all built-in adapters
60+
pub fn new() -> Self {
61+
let mut adapters: HashMap<Registry, Arc<dyn RegistryAdapter>> = HashMap::new();
62+
adapters.insert(Registry::Npm, Arc::new(NpmAdapter::new()));
63+
adapters.insert(Registry::Pypi, Arc::new(PypiAdapter::new()));
64+
Self { adapters }
65+
}
66+
67+
/// Get an adapter for a specific registry
68+
pub fn get(&self, registry: Registry) -> Option<Arc<dyn RegistryAdapter>> {
69+
self.adapters.get(&registry).cloned()
70+
}
71+
72+
/// Register a new adapter (useful for testing or custom registries)
73+
#[allow(dead_code)]
74+
pub fn register(&mut self, adapter: Arc<dyn RegistryAdapter>) {
75+
self.adapters.insert(adapter.registry(), adapter);
76+
}
77+
}
78+
79+
impl Default for AdapterRegistry {
80+
fn default() -> Self {
81+
Self::new()
82+
}
83+
}

0 commit comments

Comments
 (0)