Skip to content

Commit 98a7997

Browse files
committed
feat(backend): init api scaffold
1 parent 5a3a3e5 commit 98a7997

12 files changed

Lines changed: 224 additions & 0 deletions

File tree

apps/backend/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "verifyos-backend"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]
8+
axum = { version = "0.7", features = ["multipart"] }
9+
serde = { version = "1.0", features = ["derive"] }
10+
serde_json = "1.0"
11+
tempfile = "3.12"
12+
thiserror = "1.0"
13+
tokio = { version = "1.39", features = ["macros", "rt-multi-thread"] }
14+
tower-http = { version = "0.5", features = ["trace"] }
15+
tracing = "0.1"
16+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
17+
uuid = { version = "1.8", features = ["v4"] }
18+
19+
verifyos-cli = { path = "../../" }

apps/backend/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# verifyOS Backend (Rust)
2+
3+
This service provides a clean, versioned HTTP API that accepts an `.ipa` or `.app` upload and returns a normalized scan report.
4+
5+
## API
6+
7+
`POST /api/v1/scan`
8+
9+
- multipart `bundle` file field (required)
10+
- `profile` form field: `basic` or `full` (optional)
11+
12+
Response: JSON report (same shape as `voc --format json`).
13+
14+
## Notes
15+
16+
- This module depends on the root `verifyos-cli` crate.
17+
- It is initialized as a standalone crate for a future Cargo workspace split.

apps/backend/src/app/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod scan;
2+
3+
pub use scan::{ScanError, ScanService};

apps/backend/src/app/scan.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use crate::domain::{ScanProfileInput, ScanRequest, ScanResponse};
2+
use std::path::Path;
3+
use std::time::Instant;
4+
use thiserror::Error;
5+
use verifyos_cli::core::engine::Engine;
6+
use verifyos_cli::profiles::{register_rules, RuleSelection, ScanProfile};
7+
use verifyos_cli::report::build_report;
8+
9+
#[derive(Debug, Error)]
10+
pub enum ScanError {
11+
#[error("scan failed: {0}")]
12+
ScanFailed(String),
13+
}
14+
15+
pub struct ScanService;
16+
17+
impl ScanService {
18+
pub fn new() -> Self {
19+
Self
20+
}
21+
22+
pub fn run_scan<P: AsRef<Path>>(
23+
&self,
24+
request: ScanRequest,
25+
bundle_path: P,
26+
) -> Result<ScanResponse, ScanError> {
27+
let started = Instant::now();
28+
let profile = match request.profile {
29+
Some(ScanProfileInput::Basic) => ScanProfile::Basic,
30+
Some(ScanProfileInput::Full) | None => ScanProfile::Full,
31+
};
32+
33+
let mut engine = Engine::new();
34+
let selection = RuleSelection::default();
35+
register_rules(&mut engine, profile, &selection);
36+
37+
let run = engine
38+
.run(bundle_path)
39+
.map_err(|err| ScanError::ScanFailed(err.to_string()))?;
40+
41+
let report = build_report(&run, None);
42+
Ok(ScanResponse {
43+
report,
44+
warnings: vec![format!(
45+
"scan completed in {duration}ms",
46+
duration = started.elapsed().as_millis()
47+
)],
48+
})
49+
}
50+
}

apps/backend/src/domain/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Deserialize)]
4+
#[serde(rename_all = "snake_case")]
5+
pub enum ScanProfileInput {
6+
Basic,
7+
Full,
8+
}
9+
10+
#[derive(Debug, Deserialize)]
11+
pub struct ScanRequest {
12+
pub profile: Option<ScanProfileInput>,
13+
}
14+
15+
#[derive(Debug, Serialize)]
16+
pub struct ScanResponse {
17+
pub report: serde_json::Value,
18+
pub warnings: Vec<String>,
19+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use crate::app::{ScanError, ScanService};
2+
use crate::domain::{ScanProfileInput, ScanRequest};
3+
use axum::extract::{Multipart, State};
4+
use axum::http::StatusCode;
5+
use axum::response::IntoResponse;
6+
use serde_json::json;
7+
use std::io::Write;
8+
use tempfile::NamedTempFile;
9+
use tracing::info;
10+
11+
pub async fn health() -> impl IntoResponse {
12+
StatusCode::OK
13+
}
14+
15+
pub async fn scan_bundle(
16+
State(service): State<ScanService>,
17+
mut multipart: Multipart,
18+
) -> impl IntoResponse {
19+
let mut request = ScanRequest { profile: None };
20+
let mut temp_file: Option<NamedTempFile> = None;
21+
22+
while let Ok(Some(field)) = multipart.next_field().await {
23+
let name = field.name().unwrap_or_default().to_string();
24+
if name == "profile" {
25+
if let Ok(value) = field.text().await {
26+
request.profile = match value.to_lowercase().as_str() {
27+
"basic" => Some(ScanProfileInput::Basic),
28+
"full" => Some(ScanProfileInput::Full),
29+
_ => None,
30+
};
31+
}
32+
continue;
33+
}
34+
35+
if name == "bundle" {
36+
let mut file = match NamedTempFile::new() {
37+
Ok(file) => file,
38+
Err(err) => return to_error(err).into_response(),
39+
};
40+
let bytes = match field.bytes().await {
41+
Ok(bytes) => bytes,
42+
Err(err) => return to_error(err).into_response(),
43+
};
44+
if let Err(err) = file.write_all(&bytes) {
45+
return to_error(err).into_response();
46+
}
47+
temp_file = Some(file);
48+
}
49+
}
50+
51+
let Some(bundle) = temp_file else {
52+
return (
53+
StatusCode::BAD_REQUEST,
54+
json!({ "error": "missing bundle file field" }),
55+
)
56+
.into_response();
57+
};
58+
59+
info!("running scan for uploaded bundle");
60+
match service.run_scan(request, bundle.path()) {
61+
Ok(result) => (
62+
StatusCode::OK,
63+
serde_json::to_value(result).unwrap_or_default(),
64+
)
65+
.into_response(),
66+
Err(err) => (StatusCode::BAD_REQUEST, error_body(err)).into_response(),
67+
}
68+
}
69+
70+
fn error_body(err: ScanError) -> serde_json::Value {
71+
json!({ "error": err.to_string() })
72+
}
73+
74+
fn to_error(err: impl std::fmt::Display) -> (StatusCode, serde_json::Value) {
75+
(StatusCode::BAD_REQUEST, json!({ "error": err.to_string() }))
76+
}

apps/backend/src/infra/http/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod handlers;
2+
pub mod router;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use crate::app::ScanService;
2+
use crate::infra::http::handlers::{health, scan_bundle};
3+
use axum::routing::{get, post};
4+
use axum::Router;
5+
use tower_http::trace::TraceLayer;
6+
7+
pub fn build_router(scan_service: ScanService) -> Router {
8+
Router::new()
9+
.route("/healthz", get(health))
10+
.route("/api/v1/scan", post(scan_bundle))
11+
.with_state(scan_service)
12+
.layer(TraceLayer::new_for_http())
13+
}

apps/backend/src/infra/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod http;
2+
pub mod telemetry;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use tracing_subscriber::EnvFilter;
2+
3+
pub fn init_tracing() {
4+
let filter = EnvFilter::try_from_default_env()
5+
.unwrap_or_else(|_| EnvFilter::new("verifyos_backend=info,tower_http=info"));
6+
tracing_subscriber::fmt().with_env_filter(filter).init();
7+
}

0 commit comments

Comments
 (0)