Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Claude Code Review

on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"

jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'

runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

50 changes: 50 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Claude Code

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]

jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read

# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'

# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ build/Release
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules

test*
# test* - removed to allow test suite in test/ directory
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
Version: 0.4.0
------------
### Security Hardening
- Add input validation for all user-provided addresses in stringToS7Addr (DB number, offset, array length, bit offset)
- Add safeParseInt helper to prevent NaN/out-of-range values from silently becoming 0
- Add buffer bounds checking to prevent overflow in read/write operations
- Fix TPKT/S7 packet length validation in checkRFCData (minimum length, TPKT range validation)
- Fix logic bug in checkRFCData: changed && to || for RFC version/TPDU code check
- Complete S7 header validation in onResponse (replaces TODO at former line 1196)
- Decompose monolithic validation condition into labeled individual checks with clear error messages
- Add bounds checking in sendWritePacket to prevent data buffer overflow
- Add reportedDataLength validation in processS7Packet
- Add buffer size limit check during read optimization (prepareReadPacket)
- Add string type validation and buffer bounds checking in bufferizeS7Item
- Add security section to README with CVE references and best practices

### Connection Hardening
- Add configurable exponential backoff for reconnection (reconnectDelay, maxReconnectDelay options)
- Add maximum reconnection attempts limit (maxReconnectAttempts option)
- Track reconnect count, reset on successful connection

### New Features
- Add EventEmitter inheritance - emit 'connecting', 'connected', 'disconnected', 'reconnecting', 'connect-failed', 'error' events
- Add optional TLS support for S7-1500 (FW 2.0+) and S7-1200 (FW 4.3+) via { tls: true } option
- Add Promise/async-await API: initiateConnectionAsync, readAllItemsAsync, writeItemsAsync, dropConnectionAsync
- Add structured error codes (NodeS7.errors) for programmatic error handling

### Modernization
- Update Node.js engine requirement from "node 10.x.x" to ">=14.0.0"
- Add TypeScript type definitions (nodeS7.d.ts)
- Add test suite using Node.js built-in test runner (59 tests covering address parsing, packet validation, buffer helpers)
- Update package.json with proper engines field, types, scripts, files, and expanded keywords
- Export internal functions for testing when NODE_ENV=test

Version: 0.3.18
------------
- Add support for WDT to specify date/time which can be either UTC or local time depending on a connection parameter
Expand Down
63 changes: 54 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ WARNING
=======
Fully test everything you do. In situations where writing to a random area of memory within the PLC could cost you money, back up your data and test this really well. If this could injure someone or worse, consider other software.

Security
=======

**Important:** The S7comm protocol used by this library is inherently insecure. It was designed for isolated industrial networks and provides no built-in encryption or authentication. Be aware of the following:

* **Network Isolation**: Always use this library on isolated industrial networks or behind firewalls. Never expose S7 communication (port 102) to untrusted networks or the internet.
* **TLS Support (v0.4.0+)**: Optional TLS encryption is available for S7-1500 (FW 2.0+) and S7-1200 (FW 4.3+). Enable with `{ tls: true }` in connection parameters.
* **GET/PUT Access**: Enabling GET/PUT on S7-1200/1500 opens the controller to all applications on the network, not just this library.
* **Known CVEs**: Multiple vulnerabilities affect the S7 protocol ecosystem:
* CVE-2022-38465 (CVSS 9.3) - Private key extraction in S7-1200/1500
* CVE-2022-38773 - Firmware decryption vulnerabilities in S7-1500
* CVE-2021-37205 / CVE-2021-37185 - DoS via crafted packets on port 102
* **Recommendations**:
* Use VPN or network segmentation to protect PLC traffic
* Keep PLC firmware updated to the latest version
* Consider using TLS connections where supported
* Monitor network traffic for unauthorized access to port 102
* For new installations, consider OPC UA as a more secure alternative

Installation
=======
Using npm:
Expand Down Expand Up @@ -109,6 +128,27 @@ API
- [writeItems()](#write-items)
- [readAllItems()](#read-all-items)

### Promise/Async API (v0.4.0+)
- `initiateConnectionAsync(options)` - Returns `Promise<void>`
- `readAllItemsAsync()` - Returns `Promise<Record<string, any>>`
- `writeItemsAsync(items, values)` - Returns `Promise<void>`
- `dropConnectionAsync()` - Returns `Promise<void>`

### Events (v0.4.0+)
NodeS7 extends EventEmitter and emits the following events:
- `'connecting'` - Connection attempt started
- `'connected'` - Successfully connected and ready
- `'disconnected'` - Connection lost or closed
- `'reconnecting'` - Reconnection attempt (emits `{ attempt, delay }`)
- `'connect-failed'` - Max reconnection attempts exhausted (emits `{ attempts }`)
- `'error'` - Connection error occurred

### Error Codes (v0.4.0+)
Access via `NodeS7.errors`:
- `ERR_S7_TIMEOUT`, `ERR_S7_INVALID_ADDRESS`, `ERR_S7_BUFFER_OVERFLOW`
- `ERR_S7_PACKET_MALFORMED`, `ERR_S7_CONNECTION_REFUSED`, `ERR_S7_NOT_CONNECTED`
- `ERR_S7_WRITE_IN_PROGRESS`, `ERR_S7_PLC_ERROR`, `ERR_S7_INVALID_ARGUMENT`


## <a name="initiate-connection"></a>nodes7.initiateConnection(options, callback)
#### Description
Expand All @@ -118,15 +158,20 @@ Connects to a PLC.

`Options`

|Property|type|default|
| --- | --- | --- |
| rack | number | 0 |
| slot | number | 2 |
| port | number | 102 |
| host | string | 192.168.8.106 |
| timeout | number | 5000 |
| localTSAP | hex | undefined |
| remoteTSAP | hex | undefined |
|Property|type|default|description|
| --- | --- | --- | --- |
| rack | number | 0 | PLC rack number |
| slot | number | 2 | PLC slot (2 for S7-300/400, 1 for 1200/1500) |
| port | number | 102 | TCP port |
| host | string | 192.168.8.106 | PLC IP address |
| timeout | number | 5000 | TCP connection timeout (ms) |
| localTSAP | hex | undefined | Local TSAP for direct TSAP mode |
| remoteTSAP | hex | undefined | Remote TSAP for direct TSAP mode |
| tls | boolean | false | Enable TLS encryption (S7-1500 FW 2.0+) |
| tlsOptions | object | undefined | Options passed to tls.connect() (ca, cert, key, rejectUnauthorized) |
| maxReconnectAttempts | number | Infinity | Maximum reconnection attempts |
| reconnectDelay | number | 2000 | Base reconnection delay (ms) |
| maxReconnectDelay | number | 30000 | Maximum reconnection delay for exponential backoff (ms) |

`callback(err)`
<dl>
Expand Down
118 changes: 118 additions & 0 deletions nodeS7.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { EventEmitter } from 'events';

declare class NodeS7 extends EventEmitter {
constructor(opts?: { silent?: boolean; debug?: boolean });

/**
* Initiate a connection to a Siemens S7 PLC.
*/
initiateConnection(params: NodeS7.ConnectionParams, callback: (err?: Error) => void): void;

/**
* Drop the connection to the PLC.
*/
dropConnection(callback: () => void): void;

/**
* Set a translation callback for mapping symbolic names to S7 addresses.
*/
setTranslationCB(translator: (tag: string) => string): void;

/**
* Add items (S7 addresses) to the read polling list.
*/
addItems(items: string | string[]): void;

/**
* Remove items from the read polling list. If no items specified, removes all.
*/
removeItems(items?: string | string[]): void;

/**
* Read all items currently in the polling list.
*/
readAllItems(callback: (err: boolean, values: Record<string, any>) => void): void;

/**
* Write values to specified S7 addresses.
*/
writeItems(items: string | string[], values: any | any[], callback: (err: boolean) => void): number;

// Promise-based API

/**
* Promise-based version of initiateConnection.
*/
initiateConnectionAsync(params: NodeS7.ConnectionParams): Promise<void>;

/**
* Promise-based version of readAllItems.
*/
readAllItemsAsync(): Promise<Record<string, any>>;

/**
* Promise-based version of writeItems.
*/
writeItemsAsync(items: string | string[], values: any | any[]): Promise<void>;

/**
* Promise-based version of dropConnection.
*/
dropConnectionAsync(): Promise<void>;

// Events
on(event: 'connecting', listener: () => void): this;
on(event: 'connected', listener: () => void): this;
on(event: 'disconnected', listener: () => void): this;
on(event: 'reconnecting', listener: (info: { attempt: number; delay: number }) => void): this;
on(event: 'connect-failed', listener: (info: { attempts: number }) => void): this;
on(event: 'error', listener: (err: Error) => void): this;

// Static properties
static errors: NodeS7.S7ErrorCodes;
}

declare namespace NodeS7 {
interface ConnectionParams {
host?: string;
port?: number;
rack?: number;
slot?: number;
timeout?: number;
localTSAP?: number;
remoteTSAP?: number;
connection_name?: string;
doNotOptimize?: boolean;
wdtAsUTC?: boolean;
/** Enable TLS encryption (requires S7-1500 FW 2.0+ or S7-1200 FW 4.3+) */
tls?: boolean;
/** TLS options passed to tls.connect() */
tlsOptions?: {
ca?: Buffer | string;
cert?: Buffer | string;
key?: Buffer | string;
rejectUnauthorized?: boolean;
minVersion?: string;
};
/** Maximum number of reconnection attempts (default: Infinity) */
maxReconnectAttempts?: number;
/** Base reconnection delay in ms (default: 2000) */
reconnectDelay?: number;
/** Maximum reconnection delay in ms for exponential backoff (default: 30000) */
maxReconnectDelay?: number;
}

interface S7ErrorCodes {
TIMEOUT: string;
INVALID_ADDRESS: string;
BUFFER_OVERFLOW: string;
PACKET_MALFORMED: string;
CONNECTION_REFUSED: string;
NOT_CONNECTED: string;
WRITE_IN_PROGRESS: string;
PLC_ERROR: string;
INVALID_ARGUMENT: string;
}
}

export = NodeS7;
Loading