Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50a54c02bc | ||
|
|
0f4c75b6b3 | ||
|
|
cd65fa345c |
@@ -409,11 +409,17 @@ impl PipelineOrchestrator {
|
||||
new_count += self.run_ics_probe(target, &target_id, scan_run_id).await?;
|
||||
}
|
||||
if plc || ics {
|
||||
// PLC/SPS device: also DAST against a WebVisu / exposed endpoint. The
|
||||
// control-logic scan already consumed the code artifact, so the SAST
|
||||
// pipeline is not re-run.
|
||||
self.update_phase(scan_run_id, "dast_scanning").await;
|
||||
self.maybe_trigger_dast(&target_id, scan_run_id).await;
|
||||
// PLC/SPS device: also DAST against a WebVisu / exposed endpoint, but
|
||||
// only when DAST is actually planned — a device reachable only over an
|
||||
// industrial protocol (e.g. modbus://) has no web surface to crawl, and
|
||||
// running DAST there just fails at reconnaissance. Gating here (not only
|
||||
// at provisioning) also stops a DAST target left over from an earlier
|
||||
// run from re-triggering. The control-logic scan already consumed the
|
||||
// code artifact, so the SAST pipeline is not re-run.
|
||||
if plan.has(ScanType::Dast) {
|
||||
self.update_phase(scan_run_id, "dast_scanning").await;
|
||||
self.maybe_trigger_dast(&target_id, scan_run_id).await;
|
||||
}
|
||||
return Ok(new_count);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,12 @@ use crate::models::{ArtifactKind, OnboardedTarget, ScanType, TargetType};
|
||||
pub enum ArtifactRequirement {
|
||||
/// Source code — a git repo or a source archive.
|
||||
Code,
|
||||
/// A reachable running instance (live URL / endpoint).
|
||||
/// A reachable running instance (any live URL / endpoint, scheme-agnostic —
|
||||
/// e.g. the ICS probe works off the host:port of a modbus:// or http:// ref).
|
||||
RunningUrl,
|
||||
/// A reachable **web** endpoint — a live URL with an http(s) scheme. DAST is
|
||||
/// an HTTP crawler, so a modbus:// / opc.tcp:// endpoint does not satisfy it.
|
||||
HttpUrl,
|
||||
/// A firmware image / binary blob.
|
||||
Firmware,
|
||||
/// A PLC project (PLCopen XML or Structured Text).
|
||||
@@ -134,7 +138,7 @@ fn sast_umbrella() -> Vec<ScanRule> {
|
||||
/// The rule set for a target type. Scans that are never applicable to a type are
|
||||
/// simply absent (e.g. DAST is not listed for a PLC target).
|
||||
pub fn rules_for(target_type: TargetType) -> Vec<ScanRule> {
|
||||
use ArtifactRequirement::{Firmware, Mobile, Plc, RunningUrl};
|
||||
use ArtifactRequirement::{Firmware, HttpUrl, Mobile, Plc, RunningUrl};
|
||||
match target_type {
|
||||
TargetType::WebApp | TargetType::BackendService => {
|
||||
let mut r = sast_umbrella();
|
||||
@@ -142,7 +146,7 @@ pub fn rules_for(target_type: TargetType) -> Vec<ScanRule> {
|
||||
ScanType::Dast,
|
||||
true,
|
||||
"Dynamic scan of the running endpoint",
|
||||
RunningUrl,
|
||||
HttpUrl,
|
||||
));
|
||||
r
|
||||
}
|
||||
@@ -203,7 +207,7 @@ pub fn rules_for(target_type: TargetType) -> Vec<ScanRule> {
|
||||
ScanType::Dast,
|
||||
false,
|
||||
"Dynamic scan of exposed network services (if any)",
|
||||
RunningUrl,
|
||||
HttpUrl,
|
||||
));
|
||||
r
|
||||
}
|
||||
@@ -249,7 +253,7 @@ pub fn rules_for(target_type: TargetType) -> Vec<ScanRule> {
|
||||
ScanType::Dast,
|
||||
false,
|
||||
"Dynamic scan of the running device (WebVisu / exposed services)",
|
||||
RunningUrl,
|
||||
HttpUrl,
|
||||
),
|
||||
ScanRule::new(
|
||||
ScanType::IcsProbe,
|
||||
@@ -285,7 +289,9 @@ pub fn supports_pentest(target_type: TargetType) -> bool {
|
||||
fn representative_kind(req: ArtifactRequirement) -> Option<ArtifactKind> {
|
||||
match req {
|
||||
ArtifactRequirement::Code => Some(ArtifactKind::GitRepo),
|
||||
ArtifactRequirement::RunningUrl => Some(ArtifactKind::LiveUrl),
|
||||
ArtifactRequirement::RunningUrl | ArtifactRequirement::HttpUrl => {
|
||||
Some(ArtifactKind::LiveUrl)
|
||||
}
|
||||
ArtifactRequirement::Firmware => Some(ArtifactKind::FirmwareImage),
|
||||
ArtifactRequirement::Plc => Some(ArtifactKind::PlcProject),
|
||||
ArtifactRequirement::Mobile => Some(ArtifactKind::MobilePackage),
|
||||
@@ -294,11 +300,22 @@ fn representative_kind(req: ArtifactRequirement) -> Option<ArtifactKind> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a live-URL reference is an http(s) web endpoint (vs. an industrial
|
||||
/// endpoint like `modbus://` / `opc.tcp://`, which DAST cannot crawl).
|
||||
fn is_http_url(source_ref: &str) -> bool {
|
||||
let s = source_ref.trim();
|
||||
s.starts_with("http://") || s.starts_with("https://")
|
||||
}
|
||||
|
||||
/// Whether the target carries an artifact that satisfies the requirement.
|
||||
fn requirement_satisfied(req: ArtifactRequirement, target: &OnboardedTarget) -> bool {
|
||||
match req {
|
||||
ArtifactRequirement::Code => target.code_artifact().is_some(),
|
||||
ArtifactRequirement::RunningUrl => target.has(ArtifactKind::LiveUrl),
|
||||
ArtifactRequirement::HttpUrl => target
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|a| a.kind == ArtifactKind::LiveUrl && is_http_url(&a.source_ref)),
|
||||
ArtifactRequirement::Firmware => target.has(ArtifactKind::FirmwareImage),
|
||||
// A PLC project artifact, or a code artifact (git repo / source archive)
|
||||
// holding the control logic as PLCopen XML / ST exports — the common way
|
||||
@@ -322,6 +339,10 @@ pub fn applicable_scans(target: &OnboardedTarget) -> Vec<ScanOption> {
|
||||
let required_artifact = representative_kind(rule.requires);
|
||||
let blocked_reason = if satisfied {
|
||||
None
|
||||
} else if rule.requires == ArtifactRequirement::HttpUrl {
|
||||
// A live URL may be present but non-HTTP (e.g. modbus://): be
|
||||
// specific so the user knows DAST needs a web endpoint.
|
||||
Some("no http(s) live URL — DAST needs a web endpoint".to_string())
|
||||
} else {
|
||||
Some(match required_artifact {
|
||||
Some(kind) => format!("no {kind} artifact provided"),
|
||||
@@ -462,6 +483,49 @@ mod tests {
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plc_with_modbus_url_offers_ics_probe_but_blocks_dast() {
|
||||
// A soft-PLC reachable only over Modbus/TCP (no WebVisu). The ICS probe
|
||||
// is applicable (it works off host:port), but DAST — an HTTP crawler —
|
||||
// must be blocked so it isn't offered/run against a non-web endpoint.
|
||||
let t = target_with(
|
||||
TargetType::PlcSps,
|
||||
vec![Artifact::live_url("modbus://plc-sim:502")],
|
||||
);
|
||||
let opts = applicable_scans(&t);
|
||||
let ics = option(&opts, ScanType::IcsProbe).expect("ics probe offered");
|
||||
assert!(
|
||||
ics.blocked_reason.is_none(),
|
||||
"ICS probe should be unblocked for a modbus:// endpoint"
|
||||
);
|
||||
assert!(!ics.default_on, "ICS probe stays opt-in (default-off)");
|
||||
let dast = option(&opts, ScanType::Dast).expect("dast listed");
|
||||
assert!(
|
||||
dast.blocked_reason.is_some(),
|
||||
"DAST must be blocked without an http(s) endpoint"
|
||||
);
|
||||
assert!(!dast.default_on);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plc_with_http_webvisu_offers_both_dast_and_ics_probe() {
|
||||
// A PLC exposing a WebVisu over HTTP: both DAST (web) and the ICS probe
|
||||
// (OT ports on the same host) are applicable.
|
||||
let t = target_with(
|
||||
TargetType::PlcSps,
|
||||
vec![Artifact::live_url("http://plc.local/webvisu")],
|
||||
);
|
||||
let opts = applicable_scans(&t);
|
||||
assert!(option(&opts, ScanType::Dast)
|
||||
.expect("dast offered")
|
||||
.blocked_reason
|
||||
.is_none());
|
||||
assert!(option(&opts, ScanType::IcsProbe)
|
||||
.expect("ics probe offered")
|
||||
.blocked_reason
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pentest_support_matches_reachable_families() {
|
||||
assert!(supports_pentest(TargetType::WebApp));
|
||||
|
||||
@@ -44,6 +44,7 @@ export default withMermaid(defineConfig({
|
||||
items: [
|
||||
{ text: 'Glossary', link: '/reference/glossary' },
|
||||
{ text: 'Tools & Scanners', link: '/reference/tools' },
|
||||
{ text: 'PLC Runtime Landscape', link: '/reference/plc-runtimes' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# PLC Runtime Landscape & Support
|
||||
|
||||
A soft PLC is a **SoC + Linux + a software runtime + an IEC 61131-3 control app**
|
||||
(see [PLC / SPS Projects](/guide/plc)).
|
||||
The **runtime** is what defines the device — it provides the IEC engine, the
|
||||
Modbus / OPC UA / EtherNet/IP servers, and the WebVisu. This page tracks the
|
||||
runtime ecosystems Certifai may encounter.
|
||||
|
||||
We do **not** aim to support every runtime up front. Certifai supports the
|
||||
**CODESYS family** today; everything else is a **watch-list** — when a customer
|
||||
shows up using one, we add the parser/support for it then. The dynamic OT probe
|
||||
(Modbus / OPC UA / EtherNet/IP) is **vendor-agnostic** and works regardless of
|
||||
the runtime.
|
||||
|
||||
## Support status
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| ✅ **Supported** | Static analysis works today (control-logic SAST + library/runtime SBOM + CVE). |
|
||||
| 🟡 **Covered via CODESYS** | A rebranded CODESYS runtime — our CODESYS parsing applies (may need minor per-vendor tweaks). |
|
||||
| 🔭 **Watch-list** | Own project format — we add a format parser when a customer needs it. The dynamic OT probe already applies. |
|
||||
| 🧪 **Test-bench** | A free runtime we use to *reconstruct and dynamically test* a device (see epic: provision-and-test). |
|
||||
|
||||
## 1. CODESYS and rebranded CODESYS (the largest slice)
|
||||
|
||||
Much of the market licenses the CODESYS runtime and rebrands the IDE. If a
|
||||
customer "doesn't use CODESYS", they often do — under another name.
|
||||
|
||||
| Product / vendor | Based on | Status |
|
||||
| --- | --- | --- |
|
||||
| **CODESYS** (3S-Smart Software Solutions) | CODESYS | ✅ Supported |
|
||||
| Schneider **EcoStruxure Machine Expert** (ex-SoMachine) | CODESYS | 🟡 Covered via CODESYS |
|
||||
| **WAGO** e!COCKPIT / PFC controllers | CODESYS | 🟡 Covered via CODESYS |
|
||||
| **ABB** AC500 / Automation Builder | CODESYS | 🟡 Covered via CODESYS |
|
||||
| **Bosch Rexroth** ctrlX / IndraLogic | CODESYS | 🟡 Covered via CODESYS |
|
||||
| **Eaton** XSoft-CODESYS, **KEBA** KeStudio, Berghof, Kontron, Festo (CPX-E), IFM, Turck, … | CODESYS | 🟡 Covered via CODESYS |
|
||||
|
||||
## 2. Other embeddable IEC 61131-3 runtime toolkits
|
||||
|
||||
Same model as CODESYS (an OEM licenses a runtime + IDE and bakes it into a
|
||||
device), but with **different project formats and libraries**.
|
||||
|
||||
| Toolkit | Vendor | Status |
|
||||
| --- | --- | --- |
|
||||
| **ProConOS / MULTIPROG** | Phoenix Contact / KW-Software | 🔭 Watch-list |
|
||||
| **ISaGRAF** (also does IEC 61499) | Rockwell | 🔭 Watch-list |
|
||||
| **straton** | COPA-DATA | 🔭 Watch-list |
|
||||
| **logi.CAD** | logi.cals | 🔭 Watch-list |
|
||||
|
||||
## 3. Fully proprietary ecosystems (own runtime + IDE + protocols)
|
||||
|
||||
Static analysis here needs a **per-vendor project parser**; the **dynamic OT
|
||||
probe still works** (they speak Modbus / OPC UA / EtherNet/IP, plus vendor
|
||||
protocols like S7comm / CIP).
|
||||
|
||||
| Ecosystem | Vendor | Notes | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| **TIA Portal / STEP 7** (S7-1200/1500), S7-1500 **Software Controller**, **Virtual PLC** | Siemens | Largest install base; the soft/virtual variants are Linux/container | 🔭 Watch-list |
|
||||
| **Studio 5000** (ControlLogix / CompactLogix) | Rockwell / Allen-Bradley | Strong in North America | 🔭 Watch-list |
|
||||
| **TwinCAT 3** | Beckhoff | Genuine PC-based control on Windows / TwinCAT-BSD; IEC 61131-3 **+ C++ + Simulink** | 🔭 Watch-list |
|
||||
| **Automation Studio** | B&R (ABB) | Own Automation Runtime | 🔭 Watch-list |
|
||||
| **GX Works** (MELSEC) | Mitsubishi | | 🔭 Watch-list |
|
||||
| **Sysmac Studio** (NX / NJ) | Omron | | 🔭 Watch-list |
|
||||
| **Proficy Machine Edition** (PACSystems) | Emerson / GE | | 🔭 Watch-list |
|
||||
|
||||
## 4. Linux-native / containerized soft-PLC (the direction of travel)
|
||||
|
||||
| Product | Vendor | Notes | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| **PLCnext** | Phoenix Contact | Open, Linux-based; native runtime is eCLR (not CODESYS), but can also run CODESYS as an app | 🔭 Watch-list |
|
||||
| **ctrlX** | Bosch Rexroth | Ubuntu-core, app-store model (CODESYS runtime inside) | 🟡 Covered via CODESYS |
|
||||
| **Virtual PLC** / **CODESYS Virtual Control** | Siemens / CODESYS | Containerized PLCs (Docker / K8s) | 🟡 / 🔭 |
|
||||
|
||||
## 5. Open-source runtimes (free — our test-bench substrates)
|
||||
|
||||
Used to **reconstruct and dynamically test** a customer device without touching
|
||||
their network (provision-and-test).
|
||||
|
||||
| Runtime | Standard | Notes | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| **OpenPLC** | IEC 61131-3 | Modbus-centric, education/small automation; uses MatIEC | 🧪 Test-bench (current) |
|
||||
| **Beremiz + MatIEC** | IEC 61131-3 | Fuller open-source IDE; compiles ST/IL → C. Natural fidelity step-up from OpenPLC | 🧪 Test-bench (candidate) |
|
||||
| **Eclipse 4diac (FORTE)** | IEC **61499** | Distributed, event-driven — a *different paradigm* from 61131-3's scan cycle | 🔭 Watch-list |
|
||||
| **ProView** | — | Open-source process control + SCADA | 🔭 Watch-list |
|
||||
|
||||
## How we add support for a new runtime
|
||||
|
||||
- **Static (SAST / SBOM):** needs a parser for that runtime's **project format**
|
||||
(and its library/package convention). This is the per-vendor work.
|
||||
- **Dynamic (ICS probe / DAST):** already **vendor-agnostic** — it targets the
|
||||
device's OT ports and WebVisu, not the runtime's file format. So a brand-new
|
||||
ecosystem still gets dynamic coverage on day one.
|
||||
|
||||
::: tip Rule of thumb
|
||||
Confirm whether a "non-CODESYS" controller is actually a **rebranded CODESYS**
|
||||
runtime (Section 1) before assuming new work — most of the long tail is.
|
||||
:::
|
||||
Reference in New Issue
Block a user