cobolwork ironwork GitHub

cobolwork · Features

What cobolwork finds, and what a build does with it.

Every rule set runs on one reading of the estate: copybooks resolved, data item sizes and positions computed as the compiler computes them, and each call followed into the program it names.

What it finds

Rule sets across programs, jobs and screens

Injection, overruns, credentials and leaks are found across programs, jobs and screens.

Data flow

Finds injection that crosses program boundaries.

Untrusted input is followed through moves and CALL arguments, across COBOL and PL/I programs, to the operation it reaches.

  • Sources: the command line, a job’s PARM or in-stream data, a CICS terminal, a web request and its form fields, a CICS queue item or the data a START passes.
  • Sinks: an OS command, dynamic SQL, a dynamic CALL, LINK or XCTL, a file name, the internal reader, a subscript or length, decimal arithmetic, a record key, a log.

Checks

A route is cleared where a check protects it.

A check clears a route where it runs first, on every route, and leaves the value safe for the sink.

Runs

Finds the abends an input causes, with the input.

Each abend ironwork’s fuzzing kept is a finding at the line it happened, once the run’s journal verifies: 1,372 in 105 of 500 held-out repositories.

  • An S0C7, an S0C4, or a subscript or reference modification out of range.
  • A loop an input keeps running past the statement limit.
  • A CALL whose program an input chooses.
  • An abend in a subprogram fuzzed at its interface, which warns by default, since no run shows a caller passing those arguments.

CICS and CALL

Finds overruns and sign-on bypasses at program boundaries.

  • A communication area read without EIBCALEN.
  • A transfer to a variable program.
  • A length longer than the area or the callee.
  • A route around a sign-on.
  • A web client opened over HTTP, and a web receive longer than its area.

Screen fields

Finds records chosen by a protected screen field.

A field the BMS map protects, read back and used to choose a record, is the 3270 hidden form field.

Exfiltration

Shows where data leaves the estate.

Database rows and file records are followed out through web calls, sockets, MQ, extrapartition queues and service calls.

JCL

Finds credentials, destructive steps and leaking transfers in jobs.

  • Credentials and security commands in in-stream data.
  • Destructive statements and DLM= tricks.
  • FTP in cleartext, or sending production data.
  • Production data touched by a test job.

Secrets

Finds credentials written into programs and copybooks.

  • A literal VALUE on an item named for a credential.
  • A literal password in EXEC SQL CONNECT or EXEC CICS SIGNON.

Privilege and logs

Finds settings that widen access, and logs that leak or can be forged.

  • Diagnostic transactions installed, and command security off where it is used.
  • Credentials or personal data written to a log, and input forging a log line.
  • System error codes sent to a web client. On main, an abend’s detail and the names of the region and its resources too, never the user’s own identity.

The source

Finds code that cannot compile and source that hides what runs.

  • Names nothing declares.
  • The Communication feature, which Enterprise COBOL does not compile, with each program’s queues and statements counted.
  • Shadowed copybooks.
  • Payloads hidden in columns 73-80 or aimed at AI readers.

The estate

Finds estate details committed to source, and versions with known advisories.

  • Production names outside production jobs.
  • Routable addresses.
  • Compiler and runtime versions with published advisories.

PL/I

Finds input reaching a sink through a PL/I program, as in COBOL.

PL/I programs join the data-flow graph, and the pli set reports what the program text shows.

  • A FETCH of a module named at run time.
  • An ON unit that ignores its condition.
  • Where a pointer, an entry variable or the preprocessor takes the flow out of sight.

Limit: what a pointer, an entry variable or the preprocessor hides is listed, not followed.

IMS and Db2

Finds database access wider than the programs use.

  • A PSB granting an option its programs’ DL/I calls never use, and a segment a DL/I get retrieves reaching a sink.
  • A PSB letting a program change every segment it reads, a SENSEG naming no segment of its DBD, and a KEYLEN or field that DBDGEN, PSBGEN or ACBGEN refuses.
  • A GRANT to PUBLIC, WITH GRANT OPTION or of a system authority, and the exit routines and load modules that run inside Db2.

Limit: DL/I ISRT and REPL are not sinks yet.

Assembler and cryptography

Finds privileged assembler and weak cryptography.

  • HLASM that switches to key zero, another system key or supervisor state, runs an instruction through EX, makes a cross-memory call, loads a module named at run time, or calls the security product directly.
  • A single-length key, an MD5 or SHA-1 hash, or a fixed initialization vector asked of ICSF.

A scan reads .pli, .pl1 and .plx programs, IMS .dbd and .psb files and Db2 DDL in .sql, .ddl and .db2 files, beside COBOL, JCL, BMS and HLASM; COBOLWORK_PLI=0 leaves PL/I out. Vendor packs for CA ACF2 and Top Secret, Control-M and Connect:Direct load for estates that name them, and rules/gitleaks-mainframe.toml gives gitleaks the mainframe credential shapes it lacks. Four of these rules are offered to Betterleaks, gitleaks’s successor, in a pull request that awaits its maintainers’ review, betterleaks#379; on the 500 held-out repositories they give the same 23 findings in 9 repositories as cobolwork’s own rules. cobolwork does not depend on it: the secrets set runs without gitleaks or Betterleaks. Findings from runs are read from the manifests ironwork’s fuzzing writes, named in COBOLWORK_ABENDS.

What a finding looks like

Path across two programs

A finding gives the whole route from input to operation, with its impact and its remedy.

Here the flaw spans two files: P1 reads the command line and hands it on, and P2 runs what it was handed. Both programs are fixtures in the repository, so the output below can be reproduced.

  1. P1WS-INpos/P1.cbl:8
    Source: ACCEPT … FROM COMMAND-LINE
  2. P1WS-CMDpos/P1.cbl:9
    MOVE
  3. Crosses into P2
    P2LK-CMDpos/P1.cbl:10
    CALL 'P2' argument 1
  4. P2WS-LOCALpos/P2.cbl:9
    MOVE
  5. P2CALL 'SYSTEM' USING WS-LOCALpos/P2.cbl:10
    crit CWE-78, an OS command built from input
$ cobolwork flow test/fixtures/dataflow
{
  "rule": "argv-or-env-to-os-command",
  "sev": "crit",
  "cwe": "CWE-78",
  "path": "pos/P2.cbl",
  "line": 10,
  "program": "P2",
  "crossProgram": true,
  "hops": 4,
  "detail": "Command-line or environment input reaches an operating-system command routine: ACCEPT ... FROM COMMAND-LINE at pos/P1.cbl:8 reaches CALL 'SYSTEM' USING WS-LOCAL",
  "trace": [
    { "program": "P1", "item": "WS-IN",    "file": "pos/P1.cbl", "via": "source" },
    { "program": "P1", "item": "WS-CMD",   "file": "pos/P1.cbl", "via": "MOVE at pos/P1.cbl:9" },
    { "program": "P2", "item": "LK-CMD",   "file": "pos/P2.cbl", "via": "CALL 'P2' argument 1 at pos/P1.cbl:10" },
    { "program": "P2", "item": "WS-LOCAL", "file": "pos/P2.cbl", "via": "MOVE at pos/P2.cbl:9" }
  ],
  "related": [{ "path": "pos/P1.cbl", "line": 8, "detail": "ACCEPT ... FROM COMMAND-LINE" }],
  "sources": 1,
  "evidence": "path",
  "fingerprint": "45e55b502c397a499cc29212c0c28329"
}

Impact, carried per rule

Whoever sets the program’s command line or environment runs an arbitrary operating-system command with the program’s authority.

Remedy, carried per rule

Build the command only from fixed literals; never place input in the argument of CALL 'SYSTEM' or BPXWDYN. If it must vary, choose from an allow-list of known commands.

Fingerprint

Code added above a finding leaves its identity unchanged.

The rule, the program, the paragraph or section and the flagged statement’s text make the fingerprint, and no line number goes into it. SARIF carries it as partialFingerprints["cobolwork/v1"].

A report can be stored and passed on without the code: findings carry no source text. SARIF puts each rule’s impact in its fullDescription and the fix in its help, where GitHub code scanning shows a recommendation. Every document cobolwork writes or reads has a JSON Schema under schema/, and STABILITY.md lists what a release keeps: rule ids, the command line, exit statuses, document versions and the cobolwork/v1 fingerprint, which from 0.9.0 change only in a major release.

What a finding claims

Evidence beside severity

Each finding says who should act on it.

Severity says how urgent a finding is. evidence says what the tool established, which decides who acts, and every rule declares one kind.

EvidenceWhat the finding claimsWho acts
pathUntrusted input was traced to a sensitive operation, and trace is the routeThe program’s owner
constructThe construct is a defect wherever it sits; no input has to reach itThe owner, or whoever can rotate a credential
tamperingThe source is arranged so a reader or resolver sees something other than what runsA reviewer, before merge
advisoryA pinned compiler or runtime matches a published advisoryWhoever owns the build
exposureInformation about the estate is written into sourceThe owner
changeA change moves an interface or adds a call target (diff only)The reviewer of that change
executionA run of the program on a recorded input ended this way, and the run’s verified journal is the recordThe program’s owner
coverageThe analysis stopped following hereNobody’s code; read more, or accept the limit
contextDescribes the estate: an entry point, a product in useNobody; it asserts no defect

Limit: none of the kinds says exploitable; each path finding carries a separate exploitability verdict for that. The verdict needs who may start the transaction or job that reaches the route, which lives in RACF rather than in a repository: without the entries named in cobolwork.site.json or a reduced RACF unload in COBOLWORK_REACH, the strongest verdict is attacker-driven, and a finding names the entry points that reach it in startedBy. Only a test on the running system confirms a finding. path is a route found by reading the code, where execution is an outcome seen in a run, and its precision is measured on benchmark cases this project wrote, not yet on an independently labelled corpus.

What it could not read

Coverage reported with every scan

A short list of findings over unread source does not pass for a clean result.

A scan says what each rule set read as well as what it found. coverageIncomplete is the field to read first: an unresolved copybook, an unreadable file or a symlink leading out of the tree sets it, and so does a PL/I, IMS or Db2 statement a reader refuses, counted by kind.

Some rules need facts no repository holds, such as which dataset qualifiers are production and which DDs reach the internal reader. They go in cobolwork.site.json, which node diag/propose-site.mjs drafts from the estate’s own JCL for a person to correct. Without a fact, the rule that needs it reports under setsIncomplete that it did not run.

Starved run
26
findings, with coverageIncomplete set
Complete run
2,890
findings on the same 4,086-file repository

A scan stops before it exhausts memory, reports how much of the tree it read, and sets coverageIncomplete.

$ cobolwork scan bench/cases --quiet
"coverageIncomplete": false,
"setsIncomplete": [
  { "set": "recon", "kind": "configuration",
    "why": "no cobolwork.site.json: the production-name and production-dataset rules did not run, because nothing declares what production means in this estate" }
]

Stopping a build

Build gate

A build that adds a route to a command, a program, SQL or a job stops before it ships.

cobolwork build is a CI step, and its exit status is its verdict. Every check is the engine’s reading of the tree.

CI step
$ cobolwork build . --base origin/main -- cobc -x -o payroll PAYROLL.cbl
  • 0pass
  • 1fail
  • 3undecided
  • 4the compiler failed after a pass
  • 2it could not run

What blocks

Findings that let their author escalate privilege or change data block at any severity.

HIGH, CRIT and KNOWN-EXPLOITABLE findings block by default, and so does any finding at any tier where input chooses:

  • a command, a program, an SQL statement or a job;
  • which record is rewritten, or where in storage a write lands.

Limit: MED and LOW findings outside those two classes are reported and do not block.

A change against its base

A change is judged on what it adds, and cannot relax its own gate.

With --base, a finding blocks only if the change introduced it, except CRIT and KNOWN-EXPLOITABLE, which block wherever they are. The change is judged by the policy, waivers and site file of its base.

Limit: an incomplete scan is undecided, never a pass.

Compiler options

Programs built without subscript checking fail the gate.

A program compiled without SSRANGE, or with SSRANGE(MSG), which reports a bad subscript and carries on, fails. For cobc, the missing -fec checks are added to the command.

Policy

An organisation sets a floor that each repository can raise and never lower.

cobolwork.policy.json changes any of this, and --policy <file> names the organisation’s floor.

Precompiling

A CICS or SQL program that will not compile once translated stops the build before the compiler runs.

build --precompile -- cobc|gcobol … translates each program the compiler command names that holds EXEC SQL or EXEC CICS, and runs the same compiler on the translation with -fsyntax-only. A refused translation exits 4 and names its first errors by the program’s own lines.

Limit: a program holding EXEC DLI is left to the compiler command, and the translation is a check, never the artifact.

A z/OS download

Data sets downloaded from the mainframe are scanned, compared and gated as they stand.

scan, diff and build take --pds-export. A TSO TRANSMIT file or an IEBCOPY unload is read into its data set, and members are named DATA.SET/MEMBER. diff compares two revisions member by member, so a copybook widened in one data set is a layout change in the programs of another that copy it.

Limit: a PDSE unload, an IEBCOPY marked in error or a transmission of several files is listed and not read, and build refuses a compiler command over an export.

How accurate it is

Parser graded against GnuCOBOL

Findings rest on a parser that matches the compiler on every data item, size and call in 500 repositories it never saw.

The grade is GnuCOBOL’s own listing (cobc -t -Xref -ftsymbols), which reports every data item with the size the compiler computed, every label, the called programs, and which references write to a field. It runs on repositories never used while building the parser.

Files the compiler accepted
21,707
in 500 held-out repositories
Data items
100%
recall, at 100% precision: 845,704 of 845,704
Sizes compared
719,184
none disagree with the compiler
Labels and calls
100%
recall, at 100% precision
Held-out corpusFiles acceptedData items, recall / precisionSizesLabels and calls
100 repositories489100% / 100%0 disagree of 15,888100%
300 repositories2,21099.9% / 100%8 disagree of 94,786100% / 99.7%
500 repositories21,707100% / 100%0 disagree of 719,184100% / 100%

Measured: 100 and 300 sets 2026-09-18, 500 set 2026-10-04. On the 500 set every data item, size and call agrees with the compiler, 27,703 calls among them. Of 146,004 labels one differs, an EJECT written in Area A, which GnuCOBOL reads as a paragraph and IBM as the listing directive it is. Limit: the grade covers programs GnuCOBOL accepts, so a program with EXEC SQL or EXEC CICS is graded through the precompiler stand-in in diag/precompiler.mjs.

HLASM against z390

Assembler symbols are placed where the assembler places them, or not at all.

Graded against z390 on values no macro expansion changes, measured 2026-10-03: 2,511 of 2,546 statement locations placed in 48 development files and 1,631 of 1,675 in 76 held-out files, none placed differently. Outside conditional assembly, 99.5% of statements parse on the development corpus and 99.8% on the held-out one.

PL/I by what parses

The PL/I reader’s reach is measured, statement by statement.

No compiler may grade it, so diag/pli-measure.mjs counts what parses, measured 2026-10-07: 98.8% of 9,547 hand-written statements in the development corpus, and 99.8% of 1,768 and of 3,686 in the two held-out sets.

Limit: structure offsets are checked against hand-built fixtures and the Language Reference’s own example, not against a compiler.

Db2 DDL against IBM’s diagrams

Each DDL statement the reader refuses is accounted for.

The reader parses 6,404 of 7,039 statements in the 29 development repositories that hold DDL and 3,061 of 3,482 in the 19 held-out ones. Each statement it refuses was read against the Db2 13 for z/OS syntax diagrams and is another dialect or a form they do not draw.

Benchmark

Any scanner, cobolwork included, can be scored on the same cases.

bench/cases/ holds 153 CWE-labelled cases, each paired with a near-miss negative: the same shape with the flaw removed. node bench/run.mjs scores a scanner’s findings against them by rule and file, never by line.

npm test fails if any case scores differently from its declaration.

Validation

Every benchmark case is checked against a compiler or a grammar.

--validate compiles every COBOL case with GnuCOBOL, assembles every HLASM case with z390 when Z390 names a release, and checks every JCL case against the statement grammar. The parser tests compare against the compiler’s answers kept in test/fixtures/parser/*.golden.json, so they run without GnuCOBOL.

Limit: the JCL grammar is a weaker witness than a compiler, and the report says so.

Precision on real code comes from labels a machine made: each path finding’s verification plan run in ironwork with a marker, and flaws planted into real programs beside near-misses. An unknown is neither right nor wrong, so a rule’s precision is a range until nothing about it is unknown. A release that measures again publishes the table; the latest covers 1,896 path findings over 500 held-out repositories and seven rules on planted flaws.

Compliance

Rules mapped to DORA, FFIEC, NIST SP 800-53 and COBIT 2019

Each finding carries the clause that makes it an obligation, in each framework that has one.

Every scan carries the mapping as ruleCompliance, and SARIF carries it on each rule descriptor as properties.compliance. DORA, FFIEC and NIST are quoted verbatim, and node diag/map-compliance.mjs refuses to write a quote the cached instrument does not contain.

The same files map the evidence cobolwork and ironwork write: a run journal’s records, its chain, the provenance statement and ironwork’s options in force, each to its NIST control and COBIT practice. evidence verify names the clauses of what it checked.

Limit: COBIT 2019 is mapped by identifier alone, with this project’s own rationale. ISACA’s text is not reproduced, so reading what a practice says needs a copy of the framework.

DORA
255
rules mapped; Regulation (EU) 2022/2554
FFIEC
253
rules mapped and 2 recorded as unmapped; IT Examination Handbook
NIST SP 800-53
253
rules mapped and 2 recorded as unmapped; Rev. 5.2.0
COBIT 2019
253
rules mapped to a practice identifier and 2 recorded as unmapped

Limit: the clause choice is a judgement that no qualified assessor has reviewed. Each COBIT 2019 practice follows the rule’s NIST control through one crosswalk, so the two cannot drift apart. Where no control covers a rule, as for committing an LPAR name to a repository, the rule is recorded as unmapped with a reason.