Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f4b562c3 |
@@ -107,6 +107,8 @@ jobs:
|
|||||||
run: cargo clippy -p compliance-dashboard --features web --no-default-features -- -D warnings
|
run: cargo clippy -p compliance-dashboard --features web --no-default-features -- -D warnings
|
||||||
- name: Clippy (mcp)
|
- name: Clippy (mcp)
|
||||||
run: cargo clippy -p compliance-mcp -- -D warnings
|
run: cargo clippy -p compliance-mcp -- -D warnings
|
||||||
|
- name: Clippy (werkbank-exec)
|
||||||
|
run: cargo clippy -p werkbank-exec -- -D warnings
|
||||||
|
|
||||||
# Security audit
|
# Security audit
|
||||||
- name: Security Audit
|
- name: Security Audit
|
||||||
@@ -115,8 +117,8 @@ jobs:
|
|||||||
RUSTC_WRAPPER: ""
|
RUSTC_WRAPPER: ""
|
||||||
|
|
||||||
# Tests (reuses compilation artifacts from clippy)
|
# Tests (reuses compilation artifacts from clippy)
|
||||||
- name: Tests (core + agent)
|
- name: Tests (core + agent + werkbank-exec)
|
||||||
run: cargo test -p compliance-core -p compliance-agent --lib
|
run: cargo test -p compliance-core -p compliance-agent -p werkbank-exec --lib
|
||||||
- name: Tests (dashboard server)
|
- name: Tests (dashboard server)
|
||||||
run: cargo test -p compliance-dashboard --features server --no-default-features
|
run: cargo test -p compliance-dashboard --features server --no-default-features
|
||||||
- name: Tests (dashboard web)
|
- name: Tests (dashboard web)
|
||||||
|
|||||||
Generated
+20
@@ -699,6 +699,7 @@ dependencies = [
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
"werkbank-exec",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6714,6 +6715,25 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkbank-exec"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"compliance-core",
|
||||||
|
"compliance-dast",
|
||||||
|
"futures-util",
|
||||||
|
"hex",
|
||||||
|
"regex",
|
||||||
|
"reqwest",
|
||||||
|
"secrecy",
|
||||||
|
"sha2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ members = [
|
|||||||
"compliance-dast",
|
"compliance-dast",
|
||||||
"compliance-mcp",
|
"compliance-mcp",
|
||||||
"compliance-smoke",
|
"compliance-smoke",
|
||||||
|
"werkbank-exec",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ workspace = true
|
|||||||
compliance-core = { workspace = true, features = ["mongodb", "telemetry", "axum"] }
|
compliance-core = { workspace = true, features = ["mongodb", "telemetry", "axum"] }
|
||||||
compliance-graph = { path = "../compliance-graph" }
|
compliance-graph = { path = "../compliance-graph" }
|
||||||
compliance-dast = { path = "../compliance-dast" }
|
compliance-dast = { path = "../compliance-dast" }
|
||||||
|
# Shared dynamic-execution logic (soft-PLC provisioning + ICS probing), also
|
||||||
|
# used by the Werkbank runner.
|
||||||
|
werkbank-exec = { path = "../werkbank-exec" }
|
||||||
# Native firmware build/target detection for bare-metal & RTOS artifacts.
|
# Native firmware build/target detection for bare-metal & RTOS artifacts.
|
||||||
# Same-company IP, used directly (not via CLI) so the whole tramiton suite is
|
# Same-company IP, used directly (not via CLI) so the whole tramiton suite is
|
||||||
# available to the onboarding classifier. NOTE: CI must be able to fetch this
|
# available to the onboarding classifier. NOTE: CI must be able to fetch this
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ pub enum AgentError {
|
|||||||
#[error("Configuration error: {0}")]
|
#[error("Configuration error: {0}")]
|
||||||
Config(String),
|
Config(String),
|
||||||
|
|
||||||
|
#[error("Dynamic-execution error: {0}")]
|
||||||
|
Exec(#[from] werkbank_exec::ExecError),
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ pub mod firmware_sbom;
|
|||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod gitleaks;
|
pub mod gitleaks;
|
||||||
mod graph_build;
|
mod graph_build;
|
||||||
pub mod ics;
|
|
||||||
mod issue_creation;
|
mod issue_creation;
|
||||||
pub mod lint;
|
pub mod lint;
|
||||||
pub mod orchestrator;
|
pub mod orchestrator;
|
||||||
|
|||||||
@@ -587,7 +587,7 @@ impl PipelineOrchestrator {
|
|||||||
let path = ingest_set
|
let path = ingest_set
|
||||||
.get(&a.id)
|
.get(&a.id)
|
||||||
.and_then(|ia| ia.working_path.clone())?;
|
.and_then(|ia| ia.working_path.clone())?;
|
||||||
crate::pipeline::plc::runtime::extract_program(&path)
|
werkbank_exec::plc::extract_program(&path)
|
||||||
});
|
});
|
||||||
let Some(program) = program else {
|
let Some(program) = program else {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -597,10 +597,9 @@ impl PipelineOrchestrator {
|
|||||||
return Ok(0);
|
return Ok(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
let http = crate::pipeline::plc::runtime::http_client()?;
|
let http = werkbank_exec::plc::http_client()?;
|
||||||
let provisioner =
|
let provisioner = werkbank_exec::plc::DockerSoftPlc::new(self.config.plc_runtime.clone());
|
||||||
crate::pipeline::plc::runtime::DockerSoftPlc::new(self.config.plc_runtime.clone());
|
let outcome = werkbank_exec::plc::provision_and_test(
|
||||||
let outcome = crate::pipeline::plc::runtime::provision_and_test(
|
|
||||||
&provisioner,
|
&provisioner,
|
||||||
&http,
|
&http,
|
||||||
&self.config.plc_runtime,
|
&self.config.plc_runtime,
|
||||||
@@ -663,7 +662,7 @@ impl PipelineOrchestrator {
|
|||||||
};
|
};
|
||||||
// Short per-request budget so an unreachable device doesn't stall the scan.
|
// Short per-request budget so an unreachable device doesn't stall the scan.
|
||||||
let budget = std::time::Duration::from_secs(5);
|
let budget = std::time::Duration::from_secs(5);
|
||||||
let findings = crate::pipeline::ics::probe_target(&endpoint, target_id, budget).await;
|
let findings = werkbank_exec::ics::probe_target(&endpoint, target_id, budget).await;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
target_id,
|
target_id,
|
||||||
endpoint = %endpoint,
|
endpoint = %endpoint,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ pub mod lexer;
|
|||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod plcopen;
|
pub mod plcopen;
|
||||||
pub mod rules;
|
pub mod rules;
|
||||||
pub mod runtime;
|
|
||||||
pub mod sbom;
|
pub mod sbom;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "werkbank-exec"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Shared dynamic-execution logic: soft-PLC provisioning + industrial-protocol probing, used by the compliance agent and the Werkbank runner."
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
compliance-core = { workspace = true }
|
||||||
|
compliance-dast = { path = "../compliance-dast" }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
secrecy = { workspace = true }
|
||||||
|
sha2 = { workspace = true }
|
||||||
|
hex = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
walkdir = "2"
|
||||||
|
futures-util = "0.3"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//! Error type for the dynamic-execution logic.
|
||||||
|
|
||||||
|
/// Anything that can go wrong provisioning and testing a soft-PLC. The compliance
|
||||||
|
/// agent maps this into its own `AgentError` at the call boundary.
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ExecError {
|
||||||
|
/// An HTTP request (to OpenPLC) failed.
|
||||||
|
#[error("HTTP error: {0}")]
|
||||||
|
Http(#[from] reqwest::Error),
|
||||||
|
/// A local IO / process error (e.g. invoking `docker`).
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
/// Any other failure, with a message.
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//! Finding fingerprint helper (a SHA-256 over the salient parts), shared by the
|
||||||
|
//! probe modules for stable dedup keys. Mirrors the agent's `dedup` helper.
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
/// A stable fingerprint over the given parts (order-sensitive, separated so
|
||||||
|
/// `["ab","c"]` and `["a","bc"]` differ).
|
||||||
|
pub fn compute_fingerprint(parts: &[&str]) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
for part in parts {
|
||||||
|
hasher.update(part.as_bytes());
|
||||||
|
hasher.update(b"|");
|
||||||
|
}
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deterministic_and_hex() {
|
||||||
|
let a = compute_fingerprint(&["repo", "rule", "1"]);
|
||||||
|
assert_eq!(a, compute_fingerprint(&["repo", "rule", "1"]));
|
||||||
|
assert_eq!(a.len(), 64);
|
||||||
|
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
|
||||||
|
assert_ne!(
|
||||||
|
compute_fingerprint(&["ab", "c"]),
|
||||||
|
compute_fingerprint(&["a", "bc"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use compliance_core::models::{Finding, ScanType, Severity};
|
use compliance_core::models::{Finding, ScanType, Severity};
|
||||||
|
|
||||||
use crate::pipeline::dedup;
|
use crate::fingerprint as dedup;
|
||||||
|
|
||||||
/// Well-known deep-probe ports (each independent of any WebVisu HTTP port).
|
/// Well-known deep-probe ports (each independent of any WebVisu HTTP port).
|
||||||
const MODBUS_PORT: u16 = 502;
|
const MODBUS_PORT: u16 = 502;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//! Shared dynamic-execution logic for Werkbank.
|
||||||
|
//!
|
||||||
|
//! The soft-PLC provisioning + industrial-protocol probing that turns a control-
|
||||||
|
//! logic artifact into findings: provision an ephemeral OpenPLC, load the program,
|
||||||
|
//! start it, probe it over Modbus/OPC-UA/EtherNet-IP, DAST its web endpoint, tear
|
||||||
|
//! it down. Extracted from the compliance agent (#183) so both the agent (in
|
||||||
|
//! process) and the Werkbank runner (WB-04) run identical logic.
|
||||||
|
//!
|
||||||
|
//! - [`ics`] — read-only industrial-protocol probing.
|
||||||
|
//! - [`plc`] — ephemeral soft-PLC provisioning + the provision-and-test loop.
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod ics;
|
||||||
|
pub mod plc;
|
||||||
|
|
||||||
|
mod fingerprint;
|
||||||
|
|
||||||
|
pub use error::ExecError;
|
||||||
@@ -24,7 +24,7 @@ use compliance_core::models::dast::{DastFinding, DastScanRun, DastTarget, DastTa
|
|||||||
use compliance_core::models::Finding;
|
use compliance_core::models::Finding;
|
||||||
use compliance_core::PlcRuntimeConfig;
|
use compliance_core::PlcRuntimeConfig;
|
||||||
|
|
||||||
use crate::error::AgentError;
|
use crate::error::ExecError;
|
||||||
|
|
||||||
pub use provision::{DockerSoftPlc, ProvisionedRuntime, SoftPlc};
|
pub use provision::{DockerSoftPlc, ProvisionedRuntime, SoftPlc};
|
||||||
|
|
||||||
@@ -61,12 +61,12 @@ pub struct PlcProgram {
|
|||||||
|
|
||||||
/// A cookie-aware HTTP client for the OpenPLC web UI. A fresh client per scan
|
/// A cookie-aware HTTP client for the OpenPLC web UI. A fresh client per scan
|
||||||
/// isolates the OpenPLC session (its Flask login cookie) from every other scan.
|
/// isolates the OpenPLC session (its Flask login cookie) from every other scan.
|
||||||
pub fn http_client() -> Result<reqwest::Client, AgentError> {
|
pub fn http_client() -> Result<reqwest::Client, ExecError> {
|
||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.build()
|
.build()
|
||||||
.map_err(AgentError::Http)
|
.map_err(ExecError::Http)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pick the control-logic program to run from an ingested PLC source tree.
|
/// Pick the control-logic program to run from an ingested PLC source tree.
|
||||||
@@ -151,7 +151,7 @@ pub async fn provision_and_test<P: SoftPlc>(
|
|||||||
cfg: &PlcRuntimeConfig,
|
cfg: &PlcRuntimeConfig,
|
||||||
program: &PlcProgram,
|
program: &PlcProgram,
|
||||||
target_id: &str,
|
target_id: &str,
|
||||||
) -> Result<ProvisionOutcome, AgentError> {
|
) -> Result<ProvisionOutcome, ExecError> {
|
||||||
let handle = provisioner.provision(target_id).await?;
|
let handle = provisioner.provision(target_id).await?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
target_id,
|
target_id,
|
||||||
@@ -193,7 +193,7 @@ async fn run_dynamic_test(
|
|||||||
program: &PlcProgram,
|
program: &PlcProgram,
|
||||||
target_id: &str,
|
target_id: &str,
|
||||||
handle: &ProvisionedRuntime,
|
handle: &ProvisionedRuntime,
|
||||||
) -> Result<ProvisionOutcome, AgentError> {
|
) -> Result<ProvisionOutcome, ExecError> {
|
||||||
let ready_budget = Duration::from_secs((cfg.max_lifetime_secs / 3).clamp(10, 60));
|
let ready_budget = Duration::from_secs((cfg.max_lifetime_secs / 3).clamp(10, 60));
|
||||||
openplc::wait_ready(http, &handle.webvisu_url, ready_budget).await?;
|
openplc::wait_ready(http, &handle.webvisu_url, ready_budget).await?;
|
||||||
|
|
||||||
@@ -212,8 +212,7 @@ async fn run_dynamic_test(
|
|||||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
let probe_budget = Duration::from_secs(5);
|
let probe_budget = Duration::from_secs(5);
|
||||||
let findings =
|
let findings = crate::ics::probe_target(&handle.modbus_endpoint, target_id, probe_budget).await;
|
||||||
crate::pipeline::ics::probe_target(&handle.modbus_endpoint, target_id, probe_budget).await;
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
target_id,
|
target_id,
|
||||||
instance = %handle.name,
|
instance = %handle.name,
|
||||||
@@ -348,10 +347,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SoftPlc for FakeSoftPlc {
|
impl SoftPlc for FakeSoftPlc {
|
||||||
async fn provision(&self, _target_id: &str) -> Result<ProvisionedRuntime, AgentError> {
|
async fn provision(&self, _target_id: &str) -> Result<ProvisionedRuntime, ExecError> {
|
||||||
self.provisions.fetch_add(1, Ordering::SeqCst);
|
self.provisions.fetch_add(1, Ordering::SeqCst);
|
||||||
if self.fail_provision {
|
if self.fail_provision {
|
||||||
return Err(AgentError::Other("provision failed".into()));
|
return Err(ExecError::Other("provision failed".into()));
|
||||||
}
|
}
|
||||||
// Unreachable address so run_dynamic_test blocks on readiness until the
|
// Unreachable address so run_dynamic_test blocks on readiness until the
|
||||||
// deadline fires — exercising the teardown-on-deadline path.
|
// deadline fires — exercising the teardown-on-deadline path.
|
||||||
+15
-15
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::error::AgentError;
|
use crate::error::ExecError;
|
||||||
|
|
||||||
use super::PlcProgram;
|
use super::PlcProgram;
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ pub async fn wait_ready(
|
|||||||
http: &reqwest::Client,
|
http: &reqwest::Client,
|
||||||
base_url: &str,
|
base_url: &str,
|
||||||
budget: Duration,
|
budget: Duration,
|
||||||
) -> Result<(), AgentError> {
|
) -> Result<(), ExecError> {
|
||||||
let login = format!("{base_url}/login");
|
let login = format!("{base_url}/login");
|
||||||
let outcome = tokio::time::timeout(budget, async {
|
let outcome = tokio::time::timeout(budget, async {
|
||||||
loop {
|
loop {
|
||||||
@@ -40,7 +40,7 @@ pub async fn wait_ready(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
outcome.map_err(|_| AgentError::Other(format!("OpenPLC at {base_url} did not become ready")))
|
outcome.map_err(|_| ExecError::Other(format!("OpenPLC at {base_url} did not become ready")))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log in, upload the program, compile it, and start the runtime. On success the
|
/// Log in, upload the program, compile it, and start the runtime. On success the
|
||||||
@@ -52,7 +52,7 @@ pub async fn load_and_start(
|
|||||||
password: &str,
|
password: &str,
|
||||||
program: &PlcProgram,
|
program: &PlcProgram,
|
||||||
compile_budget: Duration,
|
compile_budget: Duration,
|
||||||
) -> Result<(), AgentError> {
|
) -> Result<(), ExecError> {
|
||||||
login(http, base_url, user, password).await?;
|
login(http, base_url, user, password).await?;
|
||||||
let prog_file = upload_program(http, base_url, program).await?;
|
let prog_file = upload_program(http, base_url, program).await?;
|
||||||
save_program(http, base_url, &prog_file).await?;
|
save_program(http, base_url, &prog_file).await?;
|
||||||
@@ -68,14 +68,14 @@ async fn login(
|
|||||||
base_url: &str,
|
base_url: &str,
|
||||||
user: &str,
|
user: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<(), AgentError> {
|
) -> Result<(), ExecError> {
|
||||||
let resp = http
|
let resp = http
|
||||||
.post(format!("{base_url}/login"))
|
.post(format!("{base_url}/login"))
|
||||||
.form(&[("username", user), ("password", password)])
|
.form(&[("username", user), ("password", password)])
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
if resp.status().is_server_error() {
|
if resp.status().is_server_error() {
|
||||||
return Err(AgentError::Other(format!(
|
return Err(ExecError::Other(format!(
|
||||||
"OpenPLC login failed: HTTP {}",
|
"OpenPLC login failed: HTTP {}",
|
||||||
resp.status()
|
resp.status()
|
||||||
)));
|
)));
|
||||||
@@ -90,7 +90,7 @@ async fn upload_program(
|
|||||||
http: &reqwest::Client,
|
http: &reqwest::Client,
|
||||||
base_url: &str,
|
base_url: &str,
|
||||||
program: &PlcProgram,
|
program: &PlcProgram,
|
||||||
) -> Result<String, AgentError> {
|
) -> Result<String, ExecError> {
|
||||||
let part = reqwest::multipart::Part::text(program.source.clone())
|
let part = reqwest::multipart::Part::text(program.source.clone())
|
||||||
.file_name(program.file_name.clone())
|
.file_name(program.file_name.clone())
|
||||||
.mime_str("application/octet-stream")?;
|
.mime_str("application/octet-stream")?;
|
||||||
@@ -102,7 +102,7 @@ async fn upload_program(
|
|||||||
.await?;
|
.await?;
|
||||||
let html = resp.text().await?;
|
let html = resp.text().await?;
|
||||||
parse_prog_file(&html).ok_or_else(|| {
|
parse_prog_file(&html).ok_or_else(|| {
|
||||||
AgentError::Other("OpenPLC upload did not return a prog_file handle".to_string())
|
ExecError::Other("OpenPLC upload did not return a prog_file handle".to_string())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ async fn save_program(
|
|||||||
http: &reqwest::Client,
|
http: &reqwest::Client,
|
||||||
base_url: &str,
|
base_url: &str,
|
||||||
prog_file: &str,
|
prog_file: &str,
|
||||||
) -> Result<(), AgentError> {
|
) -> Result<(), ExecError> {
|
||||||
let epoch = std::time::SystemTime::now()
|
let epoch = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
@@ -130,7 +130,7 @@ async fn save_program(
|
|||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
if resp.status().is_server_error() {
|
if resp.status().is_server_error() {
|
||||||
return Err(AgentError::Other(format!(
|
return Err(ExecError::Other(format!(
|
||||||
"OpenPLC save-program failed: HTTP {}",
|
"OpenPLC save-program failed: HTTP {}",
|
||||||
resp.status()
|
resp.status()
|
||||||
)));
|
)));
|
||||||
@@ -146,7 +146,7 @@ async fn compile(
|
|||||||
base_url: &str,
|
base_url: &str,
|
||||||
prog_file: &str,
|
prog_file: &str,
|
||||||
budget: Duration,
|
budget: Duration,
|
||||||
) -> Result<(), AgentError> {
|
) -> Result<(), ExecError> {
|
||||||
http.get(format!("{base_url}/compile-program"))
|
http.get(format!("{base_url}/compile-program"))
|
||||||
.query(&[("file", prog_file)])
|
.query(&[("file", prog_file)])
|
||||||
.send()
|
.send()
|
||||||
@@ -168,20 +168,20 @@ async fn compile(
|
|||||||
.await;
|
.await;
|
||||||
match outcome {
|
match outcome {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(AgentError::Other(
|
Ok(false) => Err(ExecError::Other(
|
||||||
"OpenPLC compilation finished with errors".to_string(),
|
"OpenPLC compilation finished with errors".to_string(),
|
||||||
)),
|
)),
|
||||||
Err(_) => Err(AgentError::Other(
|
Err(_) => Err(ExecError::Other(
|
||||||
"OpenPLC compilation did not finish in time".to_string(),
|
"OpenPLC compilation did not finish in time".to_string(),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `GET /start_plc` — starts the runtime, opening Modbus/TCP on 502.
|
/// `GET /start_plc` — starts the runtime, opening Modbus/TCP on 502.
|
||||||
async fn start(http: &reqwest::Client, base_url: &str) -> Result<(), AgentError> {
|
async fn start(http: &reqwest::Client, base_url: &str) -> Result<(), ExecError> {
|
||||||
let resp = http.get(format!("{base_url}/start_plc")).send().await?;
|
let resp = http.get(format!("{base_url}/start_plc")).send().await?;
|
||||||
if resp.status().is_server_error() {
|
if resp.status().is_server_error() {
|
||||||
return Err(AgentError::Other(format!(
|
return Err(ExecError::Other(format!(
|
||||||
"OpenPLC start_plc failed: HTTP {}",
|
"OpenPLC start_plc failed: HTTP {}",
|
||||||
resp.status()
|
resp.status()
|
||||||
)));
|
)));
|
||||||
+6
-6
@@ -15,7 +15,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
use compliance_core::PlcRuntimeConfig;
|
use compliance_core::PlcRuntimeConfig;
|
||||||
|
|
||||||
use crate::error::AgentError;
|
use crate::error::ExecError;
|
||||||
|
|
||||||
/// The Modbus/TCP port an OpenPLC instance opens once a program is running.
|
/// The Modbus/TCP port an OpenPLC instance opens once a program is running.
|
||||||
const MODBUS_PORT: u16 = 502;
|
const MODBUS_PORT: u16 = 502;
|
||||||
@@ -45,7 +45,7 @@ pub trait SoftPlc {
|
|||||||
fn provision(
|
fn provision(
|
||||||
&self,
|
&self,
|
||||||
target_id: &str,
|
target_id: &str,
|
||||||
) -> impl std::future::Future<Output = Result<ProvisionedRuntime, AgentError>> + Send;
|
) -> impl std::future::Future<Output = Result<ProvisionedRuntime, ExecError>> + Send;
|
||||||
|
|
||||||
/// Tear an instance down. Best-effort and idempotent — never fails the scan.
|
/// Tear an instance down. Best-effort and idempotent — never fails the scan.
|
||||||
fn teardown(&self, handle: &ProvisionedRuntime)
|
fn teardown(&self, handle: &ProvisionedRuntime)
|
||||||
@@ -65,7 +65,7 @@ impl DockerSoftPlc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SoftPlc for DockerSoftPlc {
|
impl SoftPlc for DockerSoftPlc {
|
||||||
async fn provision(&self, target_id: &str) -> Result<ProvisionedRuntime, AgentError> {
|
async fn provision(&self, target_id: &str) -> Result<ProvisionedRuntime, ExecError> {
|
||||||
// Best-effort sweep of any container leaked by a crashed earlier run
|
// Best-effort sweep of any container leaked by a crashed earlier run
|
||||||
// before we add another. Only removes instances past their max lifetime,
|
// before we add another. Only removes instances past their max lifetime,
|
||||||
// so it can never disturb a concurrent run.
|
// so it can never disturb a concurrent run.
|
||||||
@@ -75,7 +75,7 @@ impl SoftPlc for DockerSoftPlc {
|
|||||||
let args = run_args(&self.cfg, &name, target_id);
|
let args = run_args(&self.cfg, &name, target_id);
|
||||||
let out = run_docker(&args).await?;
|
let out = run_docker(&args).await?;
|
||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
return Err(AgentError::Other(format!(
|
return Err(ExecError::Other(format!(
|
||||||
"docker run for soft-PLC {name} failed: {}",
|
"docker run for soft-PLC {name} failed: {}",
|
||||||
String::from_utf8_lossy(&out.stderr).trim()
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
)));
|
)));
|
||||||
@@ -215,12 +215,12 @@ async fn reap_stale(cfg: &PlcRuntimeConfig, now: u64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run a `docker` subcommand, capturing its output.
|
/// Run a `docker` subcommand, capturing its output.
|
||||||
async fn run_docker(args: &[String]) -> Result<std::process::Output, AgentError> {
|
async fn run_docker(args: &[String]) -> Result<std::process::Output, ExecError> {
|
||||||
tokio::process::Command::new("docker")
|
tokio::process::Command::new("docker")
|
||||||
.args(args)
|
.args(args)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(AgentError::Io)
|
.map_err(ExecError::Io)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
Reference in New Issue
Block a user