Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b47b6419a |
Generated
+6
-30
@@ -694,7 +694,6 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tramiton-core",
|
"tramiton-core",
|
||||||
"tramiton-repro",
|
"tramiton-repro",
|
||||||
"tramiton-sbom",
|
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -3698,7 +3697,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3769,15 +3768,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "object"
|
|
||||||
version = "0.36.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "octocrab"
|
name = "octocrab"
|
||||||
version = "0.44.1"
|
version = "0.44.1"
|
||||||
@@ -4679,7 +4669,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6150,8 +6140,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tramiton-core"
|
name = "tramiton-core"
|
||||||
version = "0.4.1"
|
version = "0.4.0"
|
||||||
source = "git+ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git?tag=v0.4.1#ae4fc1376279f9edb9882605b20877335e7ba8ba"
|
source = "git+ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git?tag=v0.4.0#e3dc1bf7027a2f6d7b1fe43043d6dfa887ce4af3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@@ -6162,11 +6152,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tramiton-repro"
|
name = "tramiton-repro"
|
||||||
version = "0.4.1"
|
version = "0.4.0"
|
||||||
source = "git+ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git?tag=v0.4.1#ae4fc1376279f9edb9882605b20877335e7ba8ba"
|
source = "git+ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git?tag=v0.4.0#e3dc1bf7027a2f6d7b1fe43043d6dfa887ce4af3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"sha2",
|
"sha2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@@ -6175,19 +6164,6 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tramiton-sbom"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "git+ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git?tag=v0.4.1#ae4fc1376279f9edb9882605b20877335e7ba8ba"
|
|
||||||
dependencies = [
|
|
||||||
"object",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sha2",
|
|
||||||
"tramiton-core",
|
|
||||||
"tramiton-repro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter"
|
name = "tree-sitter"
|
||||||
version = "0.24.7"
|
version = "0.24.7"
|
||||||
|
|||||||
+1
-31
@@ -13,12 +13,6 @@ RUN --mount=type=secret,id=tramiton_token \
|
|||||||
fi && \
|
fi && \
|
||||||
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release -p compliance-agent
|
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release -p compliance-agent
|
||||||
|
|
||||||
# A throwaway stage that packs a real nix store (store paths + the validity DB)
|
|
||||||
# into a compressed bootstrap tarball. Only the tarball is copied into the final
|
|
||||||
# image, so we don't carry a raw /nix copy layer.
|
|
||||||
FROM nixos/nix:latest AS nixseed
|
|
||||||
RUN tar -C / -czf /nix-bootstrap.tar.gz nix
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
RUN apt-get update && apt-get install -y ca-certificates libssl3 git curl python3 python3-pip npm golang-go php-cli && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y ca-certificates libssl3 git curl python3 python3-pip npm golang-go php-cli && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -46,30 +40,7 @@ RUN pip3 install --break-system-packages semgrep
|
|||||||
# Install ruff for Python linting
|
# Install ruff for Python linting
|
||||||
RUN pip3 install --break-system-packages ruff
|
RUN pip3 install --break-system-packages ruff
|
||||||
|
|
||||||
# Real nix for the tramiton reproducible-build firmware SBOM.
|
|
||||||
#
|
|
||||||
# nix-portable's proot fallback can't run here: user namespaces are blocked by
|
|
||||||
# the container's default seccomp/apparmor profile, and orca exposes no way to
|
|
||||||
# relax it. So ship a *real* nix and disable its build sandbox
|
|
||||||
# (`sandbox = false`) — a plain gcc/make firmware build needs no user namespace,
|
|
||||||
# so it runs fine under the locked-down profile with no proot involved.
|
|
||||||
#
|
|
||||||
# The store is shipped as a bootstrap tarball and seeded onto /nix at first
|
|
||||||
# start (see docker/agent-entrypoint.sh), so a persistent /nix volume survives
|
|
||||||
# redeploys. A missing/broken nix just falls back to the analysis-only SBOM.
|
|
||||||
COPY --from=nixseed /nix-bootstrap.tar.gz /opt/nix-bootstrap.tar.gz
|
|
||||||
ENV PATH="/nix/var/nix/profiles/default/bin:${PATH}"
|
|
||||||
RUN mkdir -p /etc/nix && printf '%s\n' \
|
|
||||||
'experimental-features = nix-command flakes' \
|
|
||||||
'sandbox = false' \
|
|
||||||
'build-users-group =' \
|
|
||||||
'substituters = https://cache.nixos.org' \
|
|
||||||
'trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=' \
|
|
||||||
> /etc/nix/nix.conf
|
|
||||||
|
|
||||||
COPY --from=builder /app/target/release/compliance-agent /usr/local/bin/compliance-agent
|
COPY --from=builder /app/target/release/compliance-agent /usr/local/bin/compliance-agent
|
||||||
COPY docker/agent-entrypoint.sh /usr/local/bin/agent-entrypoint.sh
|
|
||||||
RUN chmod +x /usr/local/bin/agent-entrypoint.sh
|
|
||||||
|
|
||||||
# Copy documentation for the help chat assistant
|
# Copy documentation for the help chat assistant
|
||||||
COPY --from=builder /app/README.md /app/README.md
|
COPY --from=builder /app/README.md /app/README.md
|
||||||
@@ -81,6 +52,5 @@ RUN mkdir -p /data/compliance-scanner/ssh
|
|||||||
|
|
||||||
EXPOSE 3001 3002
|
EXPOSE 3001 3002
|
||||||
|
|
||||||
# Seeds /nix (fresh volume) from the bootstrap tarball, then runs the agent.
|
ENTRYPOINT ["compliance-agent"]
|
||||||
ENTRYPOINT ["/usr/local/bin/agent-entrypoint.sh"]
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ compliance-dast = { path = "../compliance-dast" }
|
|||||||
# 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
|
||||||
# private repo (see the git-auth step in .gitea/workflows/ci.yml).
|
# private repo (see the git-auth step in .gitea/workflows/ci.yml).
|
||||||
tramiton-core = { git = "ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git", tag = "v0.4.1" }
|
tramiton-core = { git = "ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git", tag = "v0.4.0" }
|
||||||
# tramiton-repro drives the reproducible build (NixBackend seal_and_build) that
|
# tramiton-repro's `libraries_from_inputs` turns a build plan's fetched inputs
|
||||||
# yields a sealed lock; `libraries_from_inputs` is the analysis-only fallback.
|
# into the SBOM-friendly library list (analysis-based firmware SBOM, no build).
|
||||||
tramiton-repro = { git = "ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git", tag = "v0.4.1" }
|
tramiton-repro = { git = "ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git", tag = "v0.4.0" }
|
||||||
# tramiton-sbom renders the bill of materials from a sealed lock (+ binary SCA).
|
|
||||||
tramiton-sbom = { git = "ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git", tag = "v0.4.1" }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -75,9 +75,6 @@ pub struct UpdateTargetRequest {
|
|||||||
pub scan_config: Option<TargetScanConfig>,
|
pub scan_config: Option<TargetScanConfig>,
|
||||||
pub compliance_profile: Option<ComplianceProfile>,
|
pub compliance_profile: Option<ComplianceProfile>,
|
||||||
pub scan_schedule: Option<String>,
|
pub scan_schedule: Option<String>,
|
||||||
/// Replace the target's artifacts wholesale (used by the dashboard editor).
|
|
||||||
#[serde(default)]
|
|
||||||
pub artifacts: Option<Vec<ArtifactInput>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One applicable-scan option, serialized for the wizard.
|
/// One applicable-scan option, serialized for the wizard.
|
||||||
@@ -217,13 +214,6 @@ pub async fn update_target(
|
|||||||
if let Some(ss) = req.scan_schedule {
|
if let Some(ss) = req.scan_schedule {
|
||||||
set.insert("scan_schedule", ss);
|
set.insert("scan_schedule", ss);
|
||||||
}
|
}
|
||||||
if let Some(arts) = req.artifacts {
|
|
||||||
let built: Vec<Artifact> = arts.iter().map(ArtifactInput::build).collect();
|
|
||||||
set.insert(
|
|
||||||
"artifacts",
|
|
||||||
to_bson(&built).map_err(|_| StatusCode::BAD_REQUEST)?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.onboarded_targets()
|
db.onboarded_targets()
|
||||||
.update_one(doc! { "_id": oid }, doc! { "$set": set })
|
.update_one(doc! { "_id": oid }, doc! { "$set": set })
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ pub async fn license_summary(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
summaries.sort_by_key(|s| std::cmp::Reverse(s.count));
|
summaries.sort_by(|a, b| b.count.cmp(&a.count));
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
data: summaries,
|
data: summaries,
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
//! Firmware SBOM via tramiton.
|
//! Analysis-based firmware SBOM.
|
||||||
//!
|
//!
|
||||||
//! Phase 2 (full, the default): drive a **reproducible build** with tramiton's
|
//! Derives a Software Bill of Materials for a firmware / embedded target from
|
||||||
//! `NixBackend` — `analyze` → `seal_and_build` → a sealed lock whose libraries
|
//! tramiton's build-plan analysis — the resolved external libraries and the
|
||||||
//! are pinned and whose firmware artifact carries a content hash — then render
|
//! cross-toolchain — *without* running a reproducible build. It reuses the same
|
||||||
//! the SBOM from the lock plus deep binary SCA of pre-compiled inputs. This is
|
//! `tramiton_core::provider::analyze` pass classification runs, so an SBOM comes
|
||||||
//! the complete bill of materials (toolchain + every fetched library + the
|
//! out of the source tree with no binary upload and no build toolchain in the
|
||||||
//! firmware image), the same one `tramiton sbom` produces.
|
//! agent image.
|
||||||
//!
|
//!
|
||||||
//! Phase 1 fallback (analysis-only): when no nix backend is available or the
|
//! A full reproducible-build SBOM (with artifact-level content hashes from a
|
||||||
//! build fails, fall back to the resolvable libraries + toolchain from the build
|
//! sealed `tramiton.lock`) is a later, opt-in phase — it needs tramiton's nix
|
||||||
//! plan alone (no build). A scan therefore always yields *something*, and a nix
|
//! build backend available to the agent.
|
||||||
//! that can't run in the deployment never breaks a scan.
|
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use compliance_core::models::{SbomEntry, TargetType};
|
use compliance_core::models::{SbomEntry, TargetType};
|
||||||
use tramiton_repro::ReproBackend;
|
|
||||||
use tramiton_sbom::ComponentKind;
|
|
||||||
|
|
||||||
/// Whether firmware SBOM applies to this target family.
|
/// Whether analysis-based firmware SBOM applies to this target family.
|
||||||
pub fn is_firmware_target(target_type: TargetType) -> bool {
|
pub fn is_firmware_target(target_type: TargetType) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
target_type,
|
target_type,
|
||||||
@@ -26,109 +23,37 @@ pub fn is_firmware_target(target_type: TargetType) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build SBOM entries for a firmware target from its source tree. Prefers a full
|
/// Build SBOM entries for a firmware target by analyzing its source tree with
|
||||||
/// reproducible build (sealed lock); falls back to analysis-only. Returns an
|
/// tramiton. Returns an empty vector when tramiton cannot form a build plan
|
||||||
/// empty vector when tramiton cannot even form a build plan.
|
/// (e.g. no recognizable embedded build system), so callers can treat "no
|
||||||
|
/// firmware SBOM" as simply an empty result.
|
||||||
pub async fn firmware_sbom_entries(path: &Path, repo_id: &str) -> Vec<SbomEntry> {
|
pub async fn firmware_sbom_entries(path: &Path, repo_id: &str) -> Vec<SbomEntry> {
|
||||||
let p = path.to_path_buf();
|
let p = path.to_path_buf();
|
||||||
let repo = repo_id.to_string();
|
// `analyze` is CPU-bound source inspection — keep it off the async runtime.
|
||||||
// The whole analyze → seal → build → render sequence is blocking (it shells
|
let plan = match tokio::task::spawn_blocking(move || {
|
||||||
// out to nix), so keep it off the async runtime. Bound it: a firmware build
|
let repo = tramiton_core::Repo::new(&p);
|
||||||
// that hangs must not wedge the scan (the orphaned task is abandoned).
|
tramiton_core::provider::analyze(&repo)
|
||||||
let handle = tokio::task::spawn_blocking(move || build_sbom_blocking(&p, &repo));
|
})
|
||||||
match tokio::time::timeout(std::time::Duration::from_secs(900), handle).await {
|
.await
|
||||||
Ok(Ok(entries)) => entries,
|
{
|
||||||
|
Ok(Ok(plan)) => plan,
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
tracing::warn!(repo_id, error = %e, "Firmware SBOM: task join error");
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
tracing::warn!(repo_id, "Firmware SBOM: build exceeded 15m; skipping");
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_sbom_blocking(path: &Path, repo_id: &str) -> Vec<SbomEntry> {
|
|
||||||
let repo = tramiton_core::Repo::new(path);
|
|
||||||
let plan = match tramiton_core::provider::analyze(&repo) {
|
|
||||||
Ok(Some(bp)) => bp,
|
|
||||||
Ok(None) => return Vec::new(),
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(repo_id, error = %e, "Firmware SBOM: tramiton analyze failed");
|
tracing::warn!(repo_id, error = %e, "Firmware SBOM: tramiton analyze failed");
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(repo_id, error = %e, "Firmware SBOM: analyze task join error");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(bp) = plan else {
|
||||||
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Phase 2: reproducible build → sealed lock → complete SBOM.
|
|
||||||
if let Some(backend) = tramiton_repro::NixBackend::detect() {
|
|
||||||
match tramiton_repro::seal_and_build(&backend, &plan, path) {
|
|
||||||
Ok(lock) => {
|
|
||||||
let mut sbom = tramiton_sbom::Sbom::from_lock(&lock, repo_id);
|
|
||||||
// Deep binary SCA of any pre-compiled inputs in the tree.
|
|
||||||
sbom.components.extend(tramiton_sbom::binary::scan(path));
|
|
||||||
let entries = sbom_to_entries(&sbom, repo_id);
|
|
||||||
tracing::info!(
|
|
||||||
repo_id,
|
|
||||||
backend = backend.name(),
|
|
||||||
count = entries.len(),
|
|
||||||
"Firmware SBOM: sealed reproducible build"
|
|
||||||
);
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(repo_id, error = %e, "Firmware SBOM: reproducible build failed; falling back to analysis-only")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tracing::info!(
|
|
||||||
repo_id,
|
|
||||||
"Firmware SBOM: no nix backend available; analysis-only SBOM"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 1 fallback: analysis-only (toolchain + resolvable libraries).
|
|
||||||
analysis_entries(&plan, repo_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map a rendered [`tramiton_sbom::Sbom`] (primary firmware + components) into
|
|
||||||
/// our [`SbomEntry`] rows. Source-file (`File`) components are dropped — they are
|
|
||||||
/// build inputs, not a dependency inventory.
|
|
||||||
fn sbom_to_entries(sbom: &tramiton_sbom::Sbom, repo_id: &str) -> Vec<SbomEntry> {
|
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
if let Some(primary) = &sbom.primary {
|
|
||||||
entries.push(component_to_entry(primary, repo_id));
|
|
||||||
}
|
|
||||||
for c in &sbom.components {
|
|
||||||
if matches!(c.kind, ComponentKind::File) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entries.push(component_to_entry(c, repo_id));
|
|
||||||
}
|
|
||||||
entries
|
|
||||||
}
|
|
||||||
|
|
||||||
fn component_to_entry(c: &tramiton_sbom::Component, repo_id: &str) -> SbomEntry {
|
// The cross-toolchain, recorded as a component so the SBOM captures how the
|
||||||
let manager = match c.kind {
|
// firmware is built (arm-none-eabi-gcc, zephyr-sdk, ...).
|
||||||
ComponentKind::Firmware => "firmware",
|
|
||||||
ComponentKind::Library => "library",
|
|
||||||
ComponentKind::Toolchain => "toolchain",
|
|
||||||
ComponentKind::File => "file",
|
|
||||||
};
|
|
||||||
let mut entry = SbomEntry::new(
|
|
||||||
repo_id.to_string(),
|
|
||||||
c.name.clone(),
|
|
||||||
c.version.clone().unwrap_or_default(),
|
|
||||||
manager.to_string(),
|
|
||||||
);
|
|
||||||
entry.purl = c.source.clone();
|
|
||||||
entry
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Analysis-only components from the build plan: the cross-toolchain plus the
|
|
||||||
/// resolvable fetched libraries, without a build.
|
|
||||||
fn analysis_entries(bp: &tramiton_core::BuildPlan, repo_id: &str) -> Vec<SbomEntry> {
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
if let Some(id) = bp.toolchain.id.clone() {
|
if let Some(id) = bp.toolchain.id.clone() {
|
||||||
let version = bp.toolchain.version.clone().unwrap_or_default();
|
let version = bp.toolchain.version.clone().unwrap_or_default();
|
||||||
entries.push(SbomEntry::new(
|
entries.push(SbomEntry::new(
|
||||||
@@ -138,15 +63,19 @@ fn analysis_entries(bp: &tramiton_core::BuildPlan, repo_id: &str) -> Vec<SbomEnt
|
|||||||
"toolchain".to_string(),
|
"toolchain".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolved external libraries — the SBOM-friendly view of the plan's fetched
|
||||||
|
// build inputs (name @ revision, with the upstream source when known).
|
||||||
for lib in tramiton_repro::lock::libraries_from_inputs(&bp.inputs) {
|
for lib in tramiton_repro::lock::libraries_from_inputs(&bp.inputs) {
|
||||||
let mut entry = SbomEntry::new(
|
let mut entry = SbomEntry::new(
|
||||||
repo_id.to_string(),
|
repo_id.to_string(),
|
||||||
lib.name,
|
lib.name,
|
||||||
lib.revision,
|
lib.revision,
|
||||||
"library".to_string(),
|
"tramiton".to_string(),
|
||||||
);
|
);
|
||||||
entry.purl = lib.source;
|
entry.purl = lib.source;
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
entries
|
entries
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,10 +80,7 @@ impl GitOps {
|
|||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(repo_name = %repo_name))]
|
#[tracing::instrument(skip_all, fields(repo_name = %repo_name))]
|
||||||
pub fn clone_or_fetch(&self, git_url: &str, repo_name: &str) -> Result<PathBuf, AgentError> {
|
pub fn clone_or_fetch(&self, git_url: &str, repo_name: &str) -> Result<PathBuf, AgentError> {
|
||||||
// Names can contain slashes or other path-hostile characters (a target
|
let repo_path = self.base_path.join(repo_name);
|
||||||
// named after a repo path, say); collapse to one safe directory segment
|
|
||||||
// so the clone path never nests or breaks.
|
|
||||||
let repo_path = self.base_path.join(sanitize_repo_dir(repo_name));
|
|
||||||
|
|
||||||
if repo_path.exists() {
|
if repo_path.exists() {
|
||||||
tracing::info!("fetching updates for existing repo");
|
tracing::info!("fetching updates for existing repo");
|
||||||
@@ -256,46 +253,3 @@ pub struct DiffFile {
|
|||||||
pub path: String,
|
pub path: String,
|
||||||
pub hunks: String,
|
pub hunks: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collapse a repository name into a single filesystem-safe directory segment.
|
|
||||||
/// Names may carry slashes or other path-hostile characters (a target named
|
|
||||||
/// after a repo path, for instance); those would otherwise nest or break the
|
|
||||||
/// clone path, so map anything outside `[A-Za-z0-9._-]` to `_`.
|
|
||||||
fn sanitize_repo_dir(name: &str) -> String {
|
|
||||||
let mapped: String = name
|
|
||||||
.chars()
|
|
||||||
.map(|c| {
|
|
||||||
if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
|
|
||||||
c
|
|
||||||
} else {
|
|
||||||
'_'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let trimmed = mapped.trim_matches(|c| c == '.' || c == '_');
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
"repo".to_string()
|
|
||||||
} else {
|
|
||||||
trimmed.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::sanitize_repo_dir;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sanitizes_path_hostile_names() {
|
|
||||||
assert_eq!(
|
|
||||||
sanitize_repo_dir("zephyr-example-app"),
|
|
||||||
"zephyr-example-app"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
sanitize_repo_dir("ChristianRinn/bare_metal_stm32f411xe"),
|
|
||||||
"ChristianRinn_bare_metal_stm32f411xe"
|
|
||||||
);
|
|
||||||
assert_eq!(sanitize_repo_dir("../../etc/passwd"), "etc_passwd");
|
|
||||||
assert_eq!(sanitize_repo_dir("a b:c"), "a_b_c");
|
|
||||||
assert_eq!(sanitize_repo_dir("///"), "repo");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,59 +39,6 @@ pub struct ApplicableScansResponse {
|
|||||||
pub data: ApplicableScansData,
|
pub data: ApplicableScansData,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate a target name. The name is used as the clone directory downstream,
|
|
||||||
/// so it must be a single safe segment (no slashes) and free of stray spaces.
|
|
||||||
pub fn validate_target_name(name: &str) -> Option<String> {
|
|
||||||
let n = name.trim();
|
|
||||||
if n.is_empty() {
|
|
||||||
return Some("Enter a name".to_string());
|
|
||||||
}
|
|
||||||
if name != n {
|
|
||||||
return Some("Remove the leading/trailing spaces".to_string());
|
|
||||||
}
|
|
||||||
if n.contains('/') || n.contains('\\') {
|
|
||||||
return Some("No slashes — the name becomes a folder (e.g. stm32f411-blinky)".to_string());
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Client-side validation of an artifact reference for its kind. Returns an
|
|
||||||
/// error message when the value is obviously wrong for its category, so the
|
|
||||||
/// wizard / editor can flag it up front instead of the scan discovering it.
|
|
||||||
pub fn validate_artifact_ref(kind: &str, source_ref: &str) -> Option<String> {
|
|
||||||
let s = source_ref;
|
|
||||||
if s.trim().is_empty() {
|
|
||||||
return Some("Cannot be empty".to_string());
|
|
||||||
}
|
|
||||||
if s != s.trim() {
|
|
||||||
return Some("Remove the leading/trailing spaces".to_string());
|
|
||||||
}
|
|
||||||
let no_space = !s.contains(char::is_whitespace);
|
|
||||||
match kind {
|
|
||||||
"git_repo" => {
|
|
||||||
let looks_git = s.starts_with("https://")
|
|
||||||
|| s.starts_with("http://")
|
|
||||||
|| s.starts_with("ssh://")
|
|
||||||
|| s.starts_with("git://")
|
|
||||||
|| (s.contains('@') && s.contains(':'));
|
|
||||||
(!(looks_git && no_space))
|
|
||||||
.then(|| "Enter a git URL — https://…, ssh://…, or git@host:path".to_string())
|
|
||||||
}
|
|
||||||
"live_url" => {
|
|
||||||
let ok = (s.starts_with("https://") || s.starts_with("http://")) && no_space;
|
|
||||||
(!ok).then(|| "Enter an http(s) URL, e.g. https://app.example.com".to_string())
|
|
||||||
}
|
|
||||||
"container_image" => {
|
|
||||||
(!no_space).then(|| "Enter an image ref, e.g. registry/name:tag".to_string())
|
|
||||||
}
|
|
||||||
"source_archive" | "firmware_image" | "mobile_package" | "plc_project" => {
|
|
||||||
(!no_space).then(|| "Enter a path or URL (no spaces)".to_string())
|
|
||||||
}
|
|
||||||
// plaintext_description (and anything unknown): accept free-form text.
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List onboarded targets.
|
/// List onboarded targets.
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn fetch_targets() -> Result<TargetsResponse, ServerFnError> {
|
pub async fn fetch_targets() -> Result<TargetsResponse, ServerFnError> {
|
||||||
@@ -130,41 +77,6 @@ pub async fn create_target(
|
|||||||
.map_err(|e| ServerFnError::new(e.to_string()))
|
.map_err(|e| ServerFnError::new(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a target's name / type / artifacts (dashboard editor).
|
|
||||||
#[server]
|
|
||||||
pub async fn update_target(
|
|
||||||
id: String,
|
|
||||||
name: Option<String>,
|
|
||||||
target_type: Option<String>,
|
|
||||||
artifacts: Option<Vec<ArtifactInputDto>>,
|
|
||||||
) -> Result<TargetResponse, ServerFnError> {
|
|
||||||
let mut body = serde_json::Map::new();
|
|
||||||
if let Some(n) = name {
|
|
||||||
body.insert("name".to_string(), serde_json::json!(n));
|
|
||||||
}
|
|
||||||
if let Some(t) = target_type {
|
|
||||||
body.insert("target_type".to_string(), serde_json::json!(t));
|
|
||||||
}
|
|
||||||
if let Some(a) = artifacts {
|
|
||||||
body.insert(
|
|
||||||
"artifacts".to_string(),
|
|
||||||
serde_json::to_value(a).map_err(|e| ServerFnError::new(e.to_string()))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let resp = super::agent_client::agent_request(
|
|
||||||
reqwest::Method::PATCH,
|
|
||||||
&format!("/api/v1/targets/{id}"),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.json(&serde_json::Value::Object(body))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
||||||
resp.json()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ServerFnError::new(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run kind-based classification on a target.
|
/// Run kind-based classification on a target.
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn detect_target(id: String) -> Result<TargetResponse, ServerFnError> {
|
pub async fn detect_target(id: String) -> Result<TargetResponse, ServerFnError> {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ use dioxus::prelude::*;
|
|||||||
|
|
||||||
use crate::components::page_header::PageHeader;
|
use crate::components::page_header::PageHeader;
|
||||||
use crate::infrastructure::onboarding::{
|
use crate::infrastructure::onboarding::{
|
||||||
create_target, detect_target, fetch_applicable_scans, trigger_target_scan,
|
create_target, detect_target, fetch_applicable_scans, trigger_target_scan, ArtifactInputDto,
|
||||||
validate_artifact_ref, validate_target_name, ArtifactInputDto,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// (value, label, one-line description) for the 9 target families.
|
/// (value, label, one-line description) for the 9 target families.
|
||||||
@@ -118,15 +117,8 @@ pub fn OnboardingPage() -> Element {
|
|||||||
let mut scan_msg = use_signal(|| Option::<String>::None);
|
let mut scan_msg = use_signal(|| Option::<String>::None);
|
||||||
|
|
||||||
let step_now = step();
|
let step_now = step();
|
||||||
let name_error = validate_target_name(&name());
|
let can_advance_type = !name().trim().is_empty() && !target_type().trim().is_empty();
|
||||||
let can_advance_type = name_error.is_none() && !target_type().trim().is_empty();
|
|
||||||
let has_artifacts = !artifacts().is_empty();
|
let has_artifacts = !artifacts().is_empty();
|
||||||
// Live validation of the artifact reference being typed (empty = no error yet).
|
|
||||||
let new_source_error = if new_source().is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
validate_artifact_ref(&new_kind(), &new_source())
|
|
||||||
};
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
PageHeader {
|
PageHeader {
|
||||||
@@ -165,11 +157,6 @@ pub fn OnboardingPage() -> Element {
|
|||||||
value: "{name}",
|
value: "{name}",
|
||||||
oninput: move |e| name.set(e.value()),
|
oninput: move |e| name.set(e.value()),
|
||||||
}
|
}
|
||||||
if !name().is_empty() {
|
|
||||||
if let Some(err) = name_error.clone() {
|
|
||||||
div { style: "color: var(--danger, #d33); font-size: 0.85em; margin-top: 4px;", "{err}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
style: "display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-top: 12px;",
|
style: "display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-top: 12px;",
|
||||||
@@ -226,12 +213,9 @@ pub fn OnboardingPage() -> Element {
|
|||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "btn btn-secondary",
|
class: "btn btn-secondary",
|
||||||
disabled: new_source().trim().is_empty() || new_source_error.is_some(),
|
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let kind = new_kind();
|
let kind = new_kind();
|
||||||
if !new_source().trim().is_empty()
|
if !new_source().trim().is_empty() {
|
||||||
&& validate_artifact_ref(&kind, &new_source()).is_none()
|
|
||||||
{
|
|
||||||
let branch = if kind == "git_repo" { Some(new_branch()) } else { None };
|
let branch = if kind == "git_repo" { Some(new_branch()) } else { None };
|
||||||
artifacts.write().push(ArtifactInputDto {
|
artifacts.write().push(ArtifactInputDto {
|
||||||
kind,
|
kind,
|
||||||
@@ -245,9 +229,6 @@ pub fn OnboardingPage() -> Element {
|
|||||||
"+ Add"
|
"+ Add"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(err) = new_source_error.clone() {
|
|
||||||
div { style: "color: var(--danger, #d33); font-size: 0.85em; margin-top: 6px;", "{err}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
div { style: "margin-top: 16px;",
|
div { style: "margin-top: 16px;",
|
||||||
if has_artifacts {
|
if has_artifacts {
|
||||||
|
|||||||
@@ -12,35 +12,9 @@ use dioxus_free_icons::Icon;
|
|||||||
use crate::components::page_header::PageHeader;
|
use crate::components::page_header::PageHeader;
|
||||||
use crate::components::toast::{ToastType, Toasts};
|
use crate::components::toast::{ToastType, Toasts};
|
||||||
use crate::infrastructure::onboarding::{
|
use crate::infrastructure::onboarding::{
|
||||||
delete_target, fetch_applicable_scans, fetch_targets, trigger_target_scan, update_target,
|
delete_target, fetch_applicable_scans, fetch_targets, trigger_target_scan,
|
||||||
validate_artifact_ref, validate_target_name, ArtifactInputDto,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The nine target families (value, label) for the edit form's type selector.
|
|
||||||
const TARGET_TYPES: &[(&str, &str)] = &[
|
|
||||||
("web_app", "Web Application"),
|
|
||||||
("backend_service", "Backend / API"),
|
|
||||||
("desktop_app", "Desktop App"),
|
|
||||||
("android_app", "Android App"),
|
|
||||||
("ios_app", "iOS App"),
|
|
||||||
("firmware_bare_metal", "Firmware — bare metal"),
|
|
||||||
("firmware_rtos", "Firmware — RTOS"),
|
|
||||||
("embedded_linux_yocto", "Embedded Linux / Yocto"),
|
|
||||||
("plc_sps", "PLC / SPS"),
|
|
||||||
];
|
|
||||||
|
|
||||||
/// The artifact kinds (value, label) for the edit form.
|
|
||||||
const ARTIFACT_KINDS: &[(&str, &str)] = &[
|
|
||||||
("git_repo", "Git repository"),
|
|
||||||
("source_archive", "Source archive (zip)"),
|
|
||||||
("firmware_image", "Firmware image"),
|
|
||||||
("mobile_package", "Mobile package (APK/IPA)"),
|
|
||||||
("container_image", "Container image"),
|
|
||||||
("live_url", "Live URL"),
|
|
||||||
("plc_project", "PLC project"),
|
|
||||||
("plaintext_description", "Description (text)"),
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Prettify a snake_case target-type value into a human label.
|
/// Prettify a snake_case target-type value into a human label.
|
||||||
fn pretty_type(v: &str) -> String {
|
fn pretty_type(v: &str) -> String {
|
||||||
match v {
|
match v {
|
||||||
@@ -132,17 +106,6 @@ pub fn TargetsPage() -> Element {
|
|||||||
let mut expanded_ids = use_signal(Vec::<String>::new);
|
let mut expanded_ids = use_signal(Vec::<String>::new);
|
||||||
let mut confirm_delete = use_signal(|| Option::<(String, String)>::None);
|
let mut confirm_delete = use_signal(|| Option::<(String, String)>::None);
|
||||||
|
|
||||||
// Edit-target modal state.
|
|
||||||
let mut edit_id = use_signal(|| Option::<String>::None);
|
|
||||||
let mut edit_name = use_signal(String::new);
|
|
||||||
let mut edit_type = use_signal(String::new);
|
|
||||||
let mut edit_arts = use_signal(Vec::<ArtifactInputDto>::new);
|
|
||||||
let mut edit_saving = use_signal(|| false);
|
|
||||||
// In-modal "add artifact" mini-form.
|
|
||||||
let mut e_kind = use_signal(|| "git_repo".to_string());
|
|
||||||
let mut e_source = use_signal(String::new);
|
|
||||||
let mut e_branch = use_signal(|| "main".to_string());
|
|
||||||
|
|
||||||
let mut targets = use_resource(move || async move { fetch_targets().await.ok() });
|
let mut targets = use_resource(move || async move { fetch_targets().await.ok() });
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
@@ -200,145 +163,6 @@ pub fn TargetsPage() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Edit target ──
|
|
||||||
if let Some(eid) = edit_id() {
|
|
||||||
{
|
|
||||||
let name_err = validate_target_name(&edit_name());
|
|
||||||
let e_source_err = if e_source().is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
validate_artifact_ref(&e_kind(), &e_source())
|
|
||||||
};
|
|
||||||
rsx! {
|
|
||||||
div { class: "modal-overlay",
|
|
||||||
div { class: "modal-dialog",
|
|
||||||
h3 { "Edit target" }
|
|
||||||
div { class: "form-group",
|
|
||||||
label { "Name" }
|
|
||||||
input {
|
|
||||||
r#type: "text",
|
|
||||||
value: "{edit_name}",
|
|
||||||
oninput: move |e| edit_name.set(e.value()),
|
|
||||||
}
|
|
||||||
if !edit_name().is_empty() {
|
|
||||||
if let Some(err) = name_err.clone() {
|
|
||||||
div { style: "color: var(--danger, #d33); font-size: 0.85em;", "{err}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div { class: "form-group",
|
|
||||||
label { "Type" }
|
|
||||||
select {
|
|
||||||
value: "{edit_type}",
|
|
||||||
oninput: move |e| edit_type.set(e.value()),
|
|
||||||
for (v, l) in TARGET_TYPES.iter().copied() {
|
|
||||||
option { value: "{v}", "{l}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
label { style: "font-weight: 600;", "Artifacts" }
|
|
||||||
for (i, a) in edit_arts().iter().enumerate() {
|
|
||||||
div { style: "display: flex; justify-content: space-between; align-items: center; padding: 4px 0;",
|
|
||||||
span { style: "font-size: 0.9em;",
|
|
||||||
span { style: "opacity: 0.7;", "{a.kind}: " }
|
|
||||||
span { style: "font-family: monospace;", "{a.source_ref}" }
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
class: "btn btn-ghost btn-ghost-danger btn-sm",
|
|
||||||
onclick: move |_| { edit_arts.write().remove(i); },
|
|
||||||
"Remove"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div { style: "display: flex; gap: 8px; align-items: flex-end; margin-top: 8px;",
|
|
||||||
div { class: "form-group", style: "margin: 0;",
|
|
||||||
label { "Kind" }
|
|
||||||
select {
|
|
||||||
value: "{e_kind}",
|
|
||||||
oninput: move |e| e_kind.set(e.value()),
|
|
||||||
for (v, l) in ARTIFACT_KINDS.iter().copied() {
|
|
||||||
option { value: "{v}", "{l}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div { class: "form-group", style: "margin: 0; flex: 1;",
|
|
||||||
label { "Reference" }
|
|
||||||
input {
|
|
||||||
r#type: "text",
|
|
||||||
value: "{e_source}",
|
|
||||||
oninput: move |e| e_source.set(e.value()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e_kind() == "git_repo" {
|
|
||||||
div { class: "form-group", style: "margin: 0;",
|
|
||||||
label { "Branch" }
|
|
||||||
input {
|
|
||||||
r#type: "text",
|
|
||||||
value: "{e_branch}",
|
|
||||||
oninput: move |e| e_branch.set(e.value()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
class: "btn btn-secondary",
|
|
||||||
disabled: e_source().trim().is_empty() || e_source_err.is_some(),
|
|
||||||
onclick: move |_| {
|
|
||||||
let kind = e_kind();
|
|
||||||
if !e_source().trim().is_empty()
|
|
||||||
&& validate_artifact_ref(&kind, &e_source()).is_none()
|
|
||||||
{
|
|
||||||
let branch = if kind == "git_repo" { Some(e_branch()) } else { None };
|
|
||||||
edit_arts.write().push(ArtifactInputDto {
|
|
||||||
kind,
|
|
||||||
source_ref: e_source(),
|
|
||||||
branch,
|
|
||||||
plc_format: None,
|
|
||||||
});
|
|
||||||
e_source.set(String::new());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"+ Add"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(err) = e_source_err.clone() {
|
|
||||||
div { style: "color: var(--danger, #d33); font-size: 0.85em;", "{err}" }
|
|
||||||
}
|
|
||||||
div { class: "modal-actions",
|
|
||||||
button {
|
|
||||||
class: "btn btn-secondary",
|
|
||||||
onclick: move |_| edit_id.set(None),
|
|
||||||
"Cancel"
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
class: "btn btn-primary",
|
|
||||||
disabled: edit_saving() || name_err.is_some(),
|
|
||||||
onclick: move |_| {
|
|
||||||
let id = eid.clone();
|
|
||||||
let nm = edit_name();
|
|
||||||
let tt = edit_type();
|
|
||||||
let arts = edit_arts();
|
|
||||||
edit_saving.set(true);
|
|
||||||
spawn(async move {
|
|
||||||
match update_target(id, Some(nm), Some(tt), Some(arts)).await {
|
|
||||||
Ok(_) => {
|
|
||||||
toasts.push(ToastType::Success, "Target updated");
|
|
||||||
targets.restart();
|
|
||||||
edit_id.set(None);
|
|
||||||
}
|
|
||||||
Err(e) => toasts.push(ToastType::Error, e.to_string()),
|
|
||||||
}
|
|
||||||
edit_saving.set(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
if edit_saving() { "Saving..." } else { "Save" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let targets_snapshot = targets.read().clone();
|
let targets_snapshot = targets.read().clone();
|
||||||
match &targets_snapshot {
|
match &targets_snapshot {
|
||||||
@@ -399,12 +223,8 @@ pub fn TargetsPage() -> Element {
|
|||||||
let id_scan = id.clone();
|
let id_scan = id.clone();
|
||||||
let id_exp = id.clone();
|
let id_exp = id.clone();
|
||||||
let id_del = id.clone();
|
let id_del = id.clone();
|
||||||
let id_edit = id.clone();
|
|
||||||
let name_del = name.clone();
|
let name_del = name.clone();
|
||||||
let name_edit = name.clone();
|
|
||||||
let ttype_raw = str_at(&t, "target_type").to_string();
|
|
||||||
let artifacts_detail = artifacts.clone();
|
let artifacts_detail = artifacts.clone();
|
||||||
let artifacts_edit = artifacts.clone();
|
|
||||||
rsx! {
|
rsx! {
|
||||||
tr {
|
tr {
|
||||||
td { strong { "{name}" } }
|
td { strong { "{name}" } }
|
||||||
@@ -433,31 +253,6 @@ pub fn TargetsPage() -> Element {
|
|||||||
},
|
},
|
||||||
Icon { icon: BsInfoCircle, width: 16, height: 16 }
|
Icon { icon: BsInfoCircle, width: 16, height: 16 }
|
||||||
}
|
}
|
||||||
button {
|
|
||||||
class: "btn btn-ghost",
|
|
||||||
title: "Edit target",
|
|
||||||
onclick: move |_| {
|
|
||||||
edit_name.set(name_edit.clone());
|
|
||||||
edit_type.set(ttype_raw.clone());
|
|
||||||
let arts: Vec<ArtifactInputDto> = artifacts_edit
|
|
||||||
.iter()
|
|
||||||
.map(|a| ArtifactInputDto {
|
|
||||||
kind: str_at(a, "kind").to_string(),
|
|
||||||
source_ref: str_at(a, "source_ref").to_string(),
|
|
||||||
branch: a
|
|
||||||
.get("git")
|
|
||||||
.and_then(|g| g.get("default_branch"))
|
|
||||||
.and_then(|b| b.as_str())
|
|
||||||
.map(String::from),
|
|
||||||
plc_format: None,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
edit_arts.set(arts);
|
|
||||||
e_source.set(String::new());
|
|
||||||
edit_id.set(Some(id_edit.clone()));
|
|
||||||
},
|
|
||||||
Icon { icon: BsPencil, width: 16, height: 16 }
|
|
||||||
}
|
|
||||||
button {
|
button {
|
||||||
class: if is_scanning { "btn btn-ghost btn-scanning" } else { "btn btn-ghost" },
|
class: if is_scanning { "btn btn-ghost btn-scanning" } else { "btn btn-ghost" },
|
||||||
title: "Run scan",
|
title: "Run scan",
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Seed the nix store on first start, then run the agent.
|
|
||||||
#
|
|
||||||
# The firmware-SBOM pipeline drives a real `nix` build (tramiton NixBackend).
|
|
||||||
# The image ships the store as a bootstrap tarball rather than baking /nix, so a
|
|
||||||
# persistent /nix volume (mounted empty on first deploy) gets populated once and
|
|
||||||
# then survives redeploys. Seeding is best-effort: if it fails, the agent still
|
|
||||||
# starts and firmware SBOMs fall back to analysis-only.
|
|
||||||
if [ ! -e /nix/store ]; then
|
|
||||||
echo "agent-entrypoint: seeding /nix store from image bootstrap..."
|
|
||||||
mkdir -p /nix
|
|
||||||
if tar -C / -xzf /opt/nix-bootstrap.tar.gz; then
|
|
||||||
echo "agent-entrypoint: /nix store seeded."
|
|
||||||
else
|
|
||||||
echo "agent-entrypoint: WARN nix seed failed; firmware SBOM will use analysis-only fallback."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec compliance-agent "$@"
|
|
||||||
Reference in New Issue
Block a user