Skip to content

Commit 263c494

Browse files
hyperpolymathclaude
andcommitted
feat: add Groove protocol support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 13df54b commit 263c494

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

src/groove.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
//! Gossamer Groove endpoint for panic-attacker.
5+
//!
6+
//! Exposes panic-attacker's static-analysis capabilities via the groove
7+
//! discovery protocol. Any groove-aware system (Gossamer, PanLL, Hypatia,
8+
//! etc.) can discover panic-attacker by probing GET /.well-known/groove
9+
//! on port 7600.
10+
//!
11+
//! panic-attacker works standalone as a CLI tool. When groove consumers
12+
//! connect, they gain access to the 47-language static analysis engine,
13+
//! the miniKanren logic engine, and the 20-category weak point detection.
14+
//!
15+
//! The groove connector types are formally verified in Gossamer's Groove.idr:
16+
//! - IsSubset proves consumers can only connect if panic-attacker satisfies
17+
//! their needs
18+
//! - GrooveHandle is linear: consumers MUST disconnect (no dangling grooves)
19+
//!
20+
//! ## Groove Protocol
21+
//!
22+
//! - `GET /.well-known/groove` — Capability manifest (JSON)
23+
//! - `GET /health` — Simple health check
24+
//!
25+
//! ## Capabilities Offered
26+
//!
27+
//! - `static-analysis` — 47-language static analysis with 20 weak point categories
28+
//!
29+
//! ## Capabilities Consumed (enhanced when available)
30+
//!
31+
//! - `octad-storage` (from VeriSimDB) — Persist scan results as octad entities
32+
//! - `workflow` (from CI/CD) — Trigger scans on push events
33+
34+
use std::io::{Read, Write};
35+
use std::net::{SocketAddr, TcpListener, TcpStream};
36+
37+
/// Maximum HTTP request size (16 KiB).
38+
const MAX_REQUEST_SIZE: usize = 16 * 1024;
39+
40+
/// Build the groove manifest JSON for panic-attacker.
41+
fn manifest(port: u16) -> String {
42+
format!(
43+
r#"{{
44+
"groove_version": "1",
45+
"service_id": "panic-attacker",
46+
"service_version": "{}",
47+
"capabilities": {{
48+
"static_analysis": {{
49+
"type": "static-analysis",
50+
"description": "Universal static analysis and logic-based bug signature detection for 47 languages",
51+
"protocol": "http",
52+
"endpoint": "/api/v1/scan",
53+
"requires_auth": false,
54+
"panel_compatible": true
55+
}}
56+
}},
57+
"consumes": ["octad-storage", "workflow"],
58+
"endpoints": {{
59+
"api": "http://localhost:{}/api/v1",
60+
"health": "http://localhost:{}/health"
61+
}},
62+
"health": "/health",
63+
"applicability": ["individual", "team"]
64+
}}"#,
65+
env!("CARGO_PKG_VERSION"),
66+
port,
67+
port
68+
)
69+
}
70+
71+
/// Run the groove discovery HTTP server on the given port.
72+
///
73+
/// This is a blocking synchronous server (no tokio dependency required).
74+
/// panic-attacker is primarily a CLI tool without an async runtime, so we
75+
/// use std::net for the groove endpoint. This keeps the dependency footprint
76+
/// minimal and avoids pulling in tokio for a single discovery endpoint.
77+
pub fn run(port: u16) -> anyhow::Result<()> {
78+
let addr: SocketAddr = format!("127.0.0.1:{}", port).parse()?;
79+
let listener = TcpListener::bind(addr)?;
80+
println!("[groove] panic-attacker groove endpoint listening on {}", addr);
81+
println!("[groove] Probe: curl http://localhost:{}/.well-known/groove", port);
82+
83+
for stream in listener.incoming() {
84+
match stream {
85+
Ok(mut stream) => {
86+
if let Err(e) = handle_request(&mut stream, port) {
87+
eprintln!("[groove] Request error: {}", e);
88+
}
89+
}
90+
Err(e) => {
91+
eprintln!("[groove] Accept error: {}", e);
92+
}
93+
}
94+
}
95+
96+
Ok(())
97+
}
98+
99+
/// Handle a single groove HTTP request.
100+
fn handle_request(
101+
stream: &mut TcpStream,
102+
port: u16,
103+
) -> anyhow::Result<()> {
104+
let mut buf = vec![0u8; MAX_REQUEST_SIZE];
105+
let n = stream.read(&mut buf)?;
106+
let request = std::str::from_utf8(&buf[..n])?;
107+
108+
let first_line = request.lines().next().unwrap_or("");
109+
let parts: Vec<&str> = first_line.split_whitespace().collect();
110+
if parts.len() < 2 {
111+
send_response(stream, 400, "text/plain", "Bad Request")?;
112+
return Ok(());
113+
}
114+
115+
let method = parts[0];
116+
let path = parts[1];
117+
118+
match (method, path) {
119+
// GET /.well-known/groove — Return the capability manifest.
120+
("GET", "/.well-known/groove") => {
121+
let json = manifest(port);
122+
send_response(stream, 200, "application/json", &json)?;
123+
}
124+
125+
// GET /health — Simple health check.
126+
("GET", "/health") => {
127+
send_response(
128+
stream,
129+
200,
130+
"application/json",
131+
r#"{"status":"ok","service":"panic-attacker"}"#,
132+
)?;
133+
}
134+
135+
// Unknown route.
136+
_ => {
137+
send_response(stream, 404, "text/plain", "Not Found")?;
138+
}
139+
}
140+
141+
Ok(())
142+
}
143+
144+
/// Send an HTTP response with the given content type and body.
145+
fn send_response(
146+
stream: &mut TcpStream,
147+
status: u16,
148+
content_type: &str,
149+
body: &str,
150+
) -> anyhow::Result<()> {
151+
let status_text = match status {
152+
200 => "OK",
153+
400 => "Bad Request",
154+
404 => "Not Found",
155+
_ => "Unknown",
156+
};
157+
let response = format!(
158+
"HTTP/1.0 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
159+
status,
160+
status_text,
161+
content_type,
162+
body.len(),
163+
body
164+
);
165+
stream.write_all(response.as_bytes())?;
166+
Ok(())
167+
}

src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod report;
2626
mod signatures;
2727
mod storage;
2828
mod assemblyline;
29+
mod groove;
2930
mod mass_panic;
3031
mod notify;
3132
mod types;
@@ -684,6 +685,18 @@ enum Commands {
684685
#[arg(value_enum)]
685686
shell: ShellArg,
686687
},
688+
689+
/// Start the groove discovery server for service mesh integration.
690+
///
691+
/// Runs a lightweight HTTP server exposing panic-attacker's static-analysis
692+
/// capabilities via the Gossamer groove protocol. Other groove-aware systems
693+
/// (PanLL, Gossamer, Hypatia, etc.) can discover panic-attacker by probing
694+
/// GET /.well-known/groove on the configured port.
695+
Groove {
696+
/// Port to bind the groove server to
697+
#[arg(short, long, default_value = "7600")]
698+
port: u16,
699+
},
687700
}
688701

689702
/// Patch Bridge subcommands for CVE lifecycle management.
@@ -2214,6 +2227,11 @@ fn run_main() -> Result<()> {
22142227
return Ok(());
22152228
}
22162229

2230+
Commands::Groove { port } => {
2231+
groove::run(port)?;
2232+
return Ok(());
2233+
}
2234+
22172235
Commands::Temporal { action } => {
22182236
match action {
22192237
TemporalAction::List { verisimdb_dir } => {

0 commit comments

Comments
 (0)