feat(pipeline): PLC/SPS control-logic security scanner (IEC 61131-3) (#162)
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3m48s
CI / Deploy Dashboard (push) Successful in 2m51s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m2s
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3m48s
CI / Deploy Dashboard (push) Successful in 2m51s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m2s
This commit was merged in pull request #162.
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
//! Abstract syntax tree for IEC 61131-3 Structured Text (ST).
|
||||
//!
|
||||
//! This is the security-relevant subset: POUs with their variable declarations
|
||||
//! and statement bodies, enough to run semantic control-logic rules over. It is
|
||||
//! deliberately not a full language model — declarations we don't reason about
|
||||
//! (e.g. exotic type definitions) are parsed loosely and kept as raw text.
|
||||
|
||||
/// A Program Organization Unit: a PROGRAM, FUNCTION, or FUNCTION_BLOCK.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pou {
|
||||
pub name: String,
|
||||
pub kind: PouKind,
|
||||
/// The declared variables, across all VAR_* sections.
|
||||
pub vars: Vec<VarDecl>,
|
||||
/// The statement body.
|
||||
pub body: Vec<Stmt>,
|
||||
/// 1-based line where the POU header appears (in the source that was parsed).
|
||||
pub line: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PouKind {
|
||||
Program,
|
||||
Function,
|
||||
FunctionBlock,
|
||||
}
|
||||
|
||||
impl PouKind {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
PouKind::Program => "PROGRAM",
|
||||
PouKind::Function => "FUNCTION",
|
||||
PouKind::FunctionBlock => "FUNCTION_BLOCK",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single declared variable.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VarDecl {
|
||||
pub name: String,
|
||||
pub section: VarSection,
|
||||
/// The declared type as written (e.g. `BOOL`, `INT`, `ARRAY[0..9] OF INT`).
|
||||
pub type_name: String,
|
||||
/// Whether the type is an ARRAY, and its declared bounds `(lo, hi)` when
|
||||
/// they are literal integers — used by the array-bounds rule.
|
||||
pub array_bounds: Option<(i64, i64)>,
|
||||
/// The initializer expression, if any (`:= <expr>`).
|
||||
pub init: Option<Expr>,
|
||||
pub line: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VarSection {
|
||||
Var,
|
||||
Input,
|
||||
Output,
|
||||
InOut,
|
||||
Global,
|
||||
Temp,
|
||||
External,
|
||||
}
|
||||
|
||||
/// A statement.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Stmt {
|
||||
Assign {
|
||||
target: Expr,
|
||||
value: Expr,
|
||||
line: u32,
|
||||
},
|
||||
If {
|
||||
/// (condition, body) for IF and each ELSIF, in order.
|
||||
branches: Vec<(Expr, Vec<Stmt>)>,
|
||||
else_body: Option<Vec<Stmt>>,
|
||||
line: u32,
|
||||
},
|
||||
Case {
|
||||
selector: Expr,
|
||||
/// (label expressions, body) per CASE arm.
|
||||
arms: Vec<(Vec<Expr>, Vec<Stmt>)>,
|
||||
else_body: Option<Vec<Stmt>>,
|
||||
line: u32,
|
||||
},
|
||||
For {
|
||||
var: String,
|
||||
from: Expr,
|
||||
to: Expr,
|
||||
by: Option<Expr>,
|
||||
body: Vec<Stmt>,
|
||||
line: u32,
|
||||
},
|
||||
While {
|
||||
cond: Expr,
|
||||
body: Vec<Stmt>,
|
||||
line: u32,
|
||||
},
|
||||
Repeat {
|
||||
body: Vec<Stmt>,
|
||||
until: Expr,
|
||||
line: u32,
|
||||
},
|
||||
/// A bare call statement, e.g. `TON1(IN := x, PT := T#5s);`.
|
||||
Call {
|
||||
callee: String,
|
||||
args: Vec<CallArg>,
|
||||
line: u32,
|
||||
},
|
||||
Return {
|
||||
line: u32,
|
||||
},
|
||||
Exit {
|
||||
line: u32,
|
||||
},
|
||||
/// `JMP label;` — an unstructured jump.
|
||||
Jump {
|
||||
label: String,
|
||||
line: u32,
|
||||
},
|
||||
/// `label:` — a jump target.
|
||||
Label {
|
||||
name: String,
|
||||
line: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// One argument in a call: positional (`name: None`) or named (`X := expr`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CallArg {
|
||||
pub name: Option<String>,
|
||||
pub value: Expr,
|
||||
}
|
||||
|
||||
/// An expression.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Expr {
|
||||
Int(i64, u32),
|
||||
Real(f64, u32),
|
||||
Bool(bool, u32),
|
||||
/// A string literal, with the unquoted contents.
|
||||
Str(String, u32),
|
||||
/// A duration / date / time literal, kept as raw text (`T#5s`, `DT#...`).
|
||||
Time(String, u32),
|
||||
Ident(String, u32),
|
||||
/// `base[index]`.
|
||||
Index {
|
||||
base: Box<Expr>,
|
||||
index: Box<Expr>,
|
||||
line: u32,
|
||||
},
|
||||
/// `base.field`.
|
||||
Member {
|
||||
base: Box<Expr>,
|
||||
field: String,
|
||||
line: u32,
|
||||
},
|
||||
Unary {
|
||||
op: UnOp,
|
||||
expr: Box<Expr>,
|
||||
line: u32,
|
||||
},
|
||||
Binary {
|
||||
op: BinOp,
|
||||
lhs: Box<Expr>,
|
||||
rhs: Box<Expr>,
|
||||
line: u32,
|
||||
},
|
||||
/// A function call used as an expression, e.g. `LIMIT(a, b, c)`.
|
||||
Call {
|
||||
callee: String,
|
||||
args: Vec<CallArg>,
|
||||
line: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
/// The 1-based source line this expression starts on.
|
||||
pub fn line(&self) -> u32 {
|
||||
match self {
|
||||
Expr::Int(_, l)
|
||||
| Expr::Real(_, l)
|
||||
| Expr::Bool(_, l)
|
||||
| Expr::Str(_, l)
|
||||
| Expr::Time(_, l)
|
||||
| Expr::Ident(_, l)
|
||||
| Expr::Index { line: l, .. }
|
||||
| Expr::Member { line: l, .. }
|
||||
| Expr::Unary { line: l, .. }
|
||||
| Expr::Binary { line: l, .. }
|
||||
| Expr::Call { line: l, .. } => *l,
|
||||
}
|
||||
}
|
||||
|
||||
/// If this expression is a plain identifier, its name.
|
||||
pub fn as_ident(&self) -> Option<&str> {
|
||||
match self {
|
||||
Expr::Ident(name, _) => Some(name.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UnOp {
|
||||
Not,
|
||||
Neg,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BinOp {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
Mod,
|
||||
Pow,
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
}
|
||||
Reference in New Issue
Block a user