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
PARMor in-stream data, a CICS terminal, a web request and its form fields, a CICS queue item or the data aSTARTpasses. - Sinks: an OS command, dynamic SQL, a dynamic
CALL,LINKorXCTL, 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, anS0C4, or a subscript or reference modification out of range. - A loop an input keeps running past the statement limit.
- A
CALLwhose 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
VALUEon an item named for a credential. - A literal password in
EXEC SQL CONNECTorEXEC 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
FETCHof a module named at run time. - An
ONunit 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
SENSEGnaming no segment of its DBD, and aKEYLENor field that DBDGEN, PSBGEN or ACBGEN refuses. - A
GRANTtoPUBLIC,WITH GRANT OPTIONor 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.
- P1Source:
WS-INpos/P1.cbl:8ACCEPT … FROM COMMAND-LINE - P1
WS-CMDpos/P1.cbl:9MOVE - Crosses into P2P2
LK-CMDpos/P1.cbl:10CALL 'P2'argument 1 - P2
WS-LOCALpos/P2.cbl:9MOVE - P2crit CWE-78, an OS command built from input
CALL 'SYSTEM' USING WS-LOCALpos/P2.cbl:10
{
"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 ofCALL 'SYSTEM'orBPXWDYN. 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.
| Evidence | What the finding claims | Who acts |
|---|---|---|
path | Untrusted input was traced to a sensitive operation, and trace is the route | The program’s owner |
construct | The construct is a defect wherever it sits; no input has to reach it | The owner, or whoever can rotate a credential |
tampering | The source is arranged so a reader or resolver sees something other than what runs | A reviewer, before merge |
advisory | A pinned compiler or runtime matches a published advisory | Whoever owns the build |
exposure | Information about the estate is written into source | The owner |
change | A change moves an interface or adds a call target (diff only) | The reviewer of that change |
execution | A run of the program on a recorded input ended this way, and the run’s verified journal is the record | The program’s owner |
coverage | The analysis stopped following here | Nobody’s code; read more, or accept the limit |
context | Describes the estate: an entry point, a product in use | Nobody; 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.
coverageIncomplete setA scan stops before it exhausts memory, reports how much of the tree it read, and sets coverageIncomplete.
"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.
$ 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.
| Held-out corpus | Files accepted | Data items, recall / precision | Sizes | Labels and calls |
|---|---|---|---|---|
| 100 repositories | 489 | 100% / 100% | 0 disagree of 15,888 | 100% |
| 300 repositories | 2,210 | 99.9% / 100% | 8 disagree of 94,786 | 100% / 99.7% |
| 500 repositories | 21,707 | 100% / 100% | 0 disagree of 719,184 | 100% / 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.
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.