The SCRIPT block
SCRIPT is the escape hatch. When FILTER, COLLECT, CREDMUX and
the other built-in vocabulary can't express what you need, drop a
SCRIPT block into the pipeline and write the logic in Python.
flowchart LR
upstream[Upstream block] -->|in any| script[SCRIPT]
script -->|out any| downstream[Downstream block]
The block is intentionally minimal: it has one input port in and one
output port out, both of wire type any. The shape of the data is
entirely up to you and the upstream / downstream blocks.
The process contract
Every script must define a single top-level coroutine called process
with this signature:
async def process(item, octopwn):
"""
Process a single input item.
Args:
item: One item from the 'in' port (any shape).
octopwn: The OctoPwn application context (octopwnobj).
Returns:
- A single item (forwarded on 'out'),
- A list of items (each forwarded on 'out'),
- or None (item is dropped).
"""
return item
The default code you get when you place a fresh SCRIPT block is
exactly the template above. Double-click the block in the canvas to
edit; the code is stored in the node's params['code'] field and
re-compiled at the start of each run.
A few behavioural details to internalise:
- One call per item. The engine pulls items from the
inqueue and awaitsprocess(item, octopwn)for each one. There is no batch / fan-in mode; if you need that, useCOLLECTupstream. - Lists are flattened. Returning a list lets a single input
produce multiple outputs. Returning
Nonedrops the item silently. - Exceptions abort the node. Any unhandled exception inside
processis wrapped into aRuntimeErrorwith the original traceback and marks the nodeERRORin the UI.
Configuring the block
SCRIPT exposes three parameters in the config panel:
| Parameter | Purpose |
|---|---|
code |
The Python source. Use the in-canvas editor — the IDE-style autocomplete works against octopwnobj. |
input_type |
Semantic wire type advertised on the in port. Default any. Set to the actual upstream type if you want the editor to refuse incompatible connections. |
output_type |
Semantic wire type advertised on the out port. Default any. Set this when you know exactly what your script produces — downstream FILTERs will then accept the connection. |
Setting input_type / output_type is optional but recommended for
scripts that live in shared composites — it makes the contract
explicit and lets the editor enforce it.
Example 1 — Filter to a specific share name
The built-in FILTER block evaluates a single key/op/value. When you
need a richer condition, drop into a SCRIPT block:
async def process(item, octopwn):
"""
Keep SMB share results whose UNC ends with one of a handful of
well-known sensitive share names.
"""
interesting = {'sysvol', 'netlogon', 'replication', 'admin$', 'c$'}
path = (item.get('path') or '').lower()
name = path.rstrip('\\').rsplit('\\', 1)[-1]
if name in interesting:
return item
return None
Set input_type=scan_result and output_type=scan_result so the
downstream consumer's FILTER autocompletes against the SMB share schema.
Example 2 — Enrich credentials with a custom tag
Add a __custom_tag field every downstream block can branch on:
async def process(item, octopwn):
"""Tag credentials with the source they came from for later reporting."""
enriched = dict(item)
enriched['__custom_tag'] = 'auto-dcsync-{}'.format(item.get('__cid', '?'))
return enriched
Make sure to copy the original dict before mutating — items can flow through multiple paths in parallel and mutating in place leads to spooky-action-at-a-distance bugs.
What you have access to
The octopwn argument is the live octopwnobj — the same object every
other block uses. From inside a script you can:
- Read / write the credential store via
octopwn.credentialMgr. - Read / write the target store via
octopwn.targetMgr. - Query open sessions via
octopwn.sessionMgr. - Trigger any of the helpers that the regular client / scanner / attack code uses internally.
That makes the SCRIPT block effectively unlimited in power and, by the same token, unsandboxed. The same trust model as the OctoPwn IDE window applies — if you didn't write the script, read it before you run it.
When to not use a script
If your script ends up being more than ~30 lines and looks like it
might be useful elsewhere, the right tool is usually a
plugin, not a script block. Plugins live
in ~/.octopwn/plugins/, get autoloaded by PLUGINLOADER, can ship
their own UI, and are far easier to share with the team.
SCRIPT is for the cases where the logic is so specific to one
flowgraph that turning it into a plugin would just create paperwork.