CyberChef's core ops carry the highest activity risk — 5 functions to address first

Analysis of gchq/CyberChef at bd4c59c finds five critical-band functions combining cyclomatic complexity as high as 102 with recent commit activity, concentrated in core encoding, cryptanalysis, and coordinate-conversion logic.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk19.46Low
Hottest Functionrun

Antipatterns Detected

complex_branching5exit_heavy5deeply_nested3long_function3god_function3hub_function1

Run this on your own codebase

Hotspots runs locally in under a minute — no account, no data leaves your machine.

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is complex branching and why does it matter in CyberChef?

Complex branching is a pattern where a function contains a large number of independent conditional paths — measured by cyclomatic complexity, which counts the minimum number of test cases needed to exercise every path at least once. A cyclomatic complexity of 10 is considered moderate; above 30 it becomes very difficult to reason about comprehensively; above 100 it is effectively untestable in practice without deliberate decomposition. In CyberChef, all five top hotspots carry this pattern, with the `run` function in `ParseIPv6Address.mjs` reaching a cyclomatic complexity of 102 — meaning over a hundred distinct execution paths through a single function. Every one of those paths is a potential site for a regression when the function is modified, and the absence of a test covering any one of them is a latent defect waiting to be triggered.

How do I reduce cyclomatic complexity in JavaScript?

The most effective technique is extract-method refactoring: identify cohesive groups of branches that share a single responsibility and pull them into named helper functions. A cyclomatic complexity above 15 is a reasonable trigger for splitting; above 30 it warrants immediate action. For `run` in `ParseIPv6Address.mjs` (CC 102), I would start by extracting each reserved-address classification — Teredo, IPv4-mapped, IPv4-translated, and so on — into its own function such as `classifyTeredoAddress(ipv6)`. Each extracted function takes a small, well-defined input and returns a string fragment, making it independently testable. Doing this for five or six branches alone would reduce the CC of the outer `run` function by roughly half and give the team a set of focused unit tests that currently don't exist.

Is CyberChef actively maintained?

Yes — the quadrant data is clear on this. At commit bd4c59c, 609 of CyberChef's 2,010 analysed functions sit in the "fire" quadrant, meaning they combine high structural complexity with recent commit activity. All five of the top hotspots were touched within the last 8 days, each recording 1 commit in the last 30 days. Only 2 functions sit in the "debt" quadrant (complex but dormant), which is a remarkably low number for a project of this size. Active development and significant structural complexity are not mutually exclusive — CyberChef is clearly being worked on, which is precisely why the fire-quadrant functions deserve attention now rather than later.

How do I reproduce this analysis?

The analysis was run against gchq/CyberChef at commit `bd4c59c`. After checking out that commit with `git checkout bd4c59c`, run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root using the Hotspots CLI (available at github.com/hotspots-dev/hotspots). The same command works on any local git repository without additional configuration, and the `--explain-patterns` flag produces the antipattern annotations shown in this post.

What does activity-weighted risk mean?

Activity-weighted risk combines a function's structural complexity — derived from its cyclomatic complexity, maximum nesting depth, and fan-out — with how frequently it has been modified in recent commits. A function with a cyclomatic complexity of 102 that has sat untouched for two years scores lower than one with a cyclomatic complexity of 33 that is being edited every week, because the dormant function, however complex, is not currently a site where regressions are being introduced. The score prioritises functions where structural difficulty and active development overlap — the conditions under which bugs are most likely to be written and hardest to catch in review. For CyberChef, all five top hotspots sit in that overlap right now.

At commit bd4c59c, CyberChef has 2,010 analysed functions, 242 of which fall in the critical band. Five of those sit at the intersection of high structural complexity and recent commit activity — the “fire” quadrant — meaning they are live regression risks right now, not cleanup items for a future sprint. I would start with run in ParseIPv6Address.mjs: an activity-weighted risk score of 19.46, a cyclomatic complexity of 102, and a last-modified date of 8 days ago make it the most urgent surface in the codebase. CyberChef is GCHQ’s browser-based data-transformation tool, and the breadth of operations it supports — from cryptanalysis to coordinate conversion to encoding primitives — is exactly what makes these hotspots consequential: they sit on paths that many other operations depend on.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
runsrc/core/operations/ParseIPv6Address.mjs19.5102198
convertCoordinatessrc/core/lib/ConvertCoordinates.mjs17.581437
runsrc/core/operations/MultipleBombe.mjs17.125619
_addQPSoftLinebreakssrc/core/operations/ToQuotedPrintable.mjs17.12387
fromBase64src/core/lib/Base64.mjs16.833410

Large Repo Analysis

CyberChef is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.

Repository snapshot

Triage Band Distribution
Fire609Debt2Watch1395OK4

2,010 functions analyzed

The quadrant distribution tells an immediate story: 609 functions are in the “fire” quadrant — high complexity and active recent commits — while only 2 sit in “debt” (complex but dormant). The overwhelming majority of CyberChef’s structural risk is live, not parked. That’s the baseline before we look at which specific functions sit at the top of the stack.

Detected Antipatterns
Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
Deeply Nested×3Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
Long Function×3Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
God Function×3God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.

Across the five hotspots, complex branching and exit-heavy patterns appear in every single one. Three are flagged as god functions, meaning they accumulate logic that would be better distributed across focused helpers. Three are also long functions, a direct invitation for extract-method refactoring. One carries the hub-function pattern — more on that below.


run — ParseIPv6Address.mjs

run
src/core/operations/ParseIPv6Address.mjs
19.46
fire
CC 102
ND 19
FO 8
touches/30d 1

This function handles the full classification of an IPv6 address: it matches the input against a regex, converts it to longhand and shorthand forms, then works through a long chain of conditionals to identify reserved address types — unspecified, loopback, IPv4-mapped, IPv4-translated, discard prefix, well-known translation prefix, Teredo tunneling, and more. Each branch appends a different block of explanatory text to the output and, in the Teredo case, unpacks several bit-field flags with their own nested conditionals.

The numbers are striking. A cyclomatic complexity of 102 means there are at least 102 independent execution paths through this function — each one a required test case and a potential bug surface. The maximum nesting depth of 19 is the most extreme value in the top five; at that depth, a reader tracking the current conditional context has to hold nearly 20 layers of state in their head simultaneously. This is the complex_branching and deeply_nested pattern combination in its most severe form.

Cyclomatic Complexity 102
threshold: 10
Max Nesting Depth 19
threshold: 4

The function was touched 8 days ago — a single commit in the last 30 days that is also the file’s only commit on record. There are no bug-linked commits or reverts in the external signals, so there is no historical defect evidence to cite. But a CC of 102 with a nesting depth of 19 is a structural argument on its own: the next developer to modify this function, for any reason, will be reasoning inside one of the most complex single functions in the repository.

My recommendation: decompose by address family. Each reserved-address branch — Teredo, IPv4-mapped, IPv4-translated, discard prefix, well-known prefix — is self-contained enough to become its own helper function. Extracting five or six named helpers would halve the CC of run while making each address-type handler independently testable. The Teredo branch alone, with its bit-field unpacking and flag validation, is a strong candidate for immediate extraction.


convertCoordinates — ConvertCoordinates.mjs

convertCoordinates
src/core/lib/ConvertCoordinates.mjs
17.54
fire
CC 81
ND 4
FO 37
touches/30d 1

This is a library-level function — not inside an operations/ file but in lib/, meaning other operations call it directly. It accepts input in one coordinate format and delimiter, converts to a common geodesy lat/lon object, then converts out to a target format and delimiter. The source excerpt shows a large switch statement over input formats, with each case delegating to a different parsing library (geohash, MGRS, OS Grid Reference, and others).

The headline number here is the fan-out of 37 — the highest in the top five by a wide margin. Fan-out counts distinct functions called, and 37 means this function directly invokes 37 different callees. In JavaScript, where dynamic property access and prototype dispatch can obscure dependencies that static analysis misses, 37 is likely a floor, not a ceiling. The god_function and long_function patterns both flag here, which is consistent: a function that orchestrates 37 callees across multiple coordinate-system libraries is doing far more than one thing.

Fan-Out (distinct callees) 37
threshold: 15
Cyclomatic Complexity 81
threshold: 10

The nesting depth of 4 is comparatively manageable — the complexity here is horizontal (breadth of coupling) rather than vertical (depth of nesting). But that makes it no less risky: a change to how any one coordinate format is parsed, or to the delimiter-detection logic shared across formats, has blast radius across all 37 callees and every operation that calls convertCoordinates.

The file has a single commit and one author in the last 90 days, with no bug-linked commits or reverts. The immediate action I would take is to introduce a format-handler registry — a map from format name to a parser function — and extract each case in the switch into its own named converter. That collapses the switch to a single dispatch call, reduces the fan-out of the top-level function significantly, and makes each format independently testable without loading the full coordinate conversion pipeline.


run — MultipleBombe.mjs

run
src/core/operations/MultipleBombe.mjs
17.1
fire
CC 25
ND 6
FO 19
touches/30d 1

The Multiple Bombe operation simulates a brute-force search over Enigma machine rotor, reflector, and fourth-rotor combinations, looking for configurations that produce the crib (known plaintext) from a given ciphertext. The source excerpt confirms what the name implies: a set of deeply nested for loops — one per rotor position — with early-continue guards to skip duplicate rotor assignments. The comment in the source is candid: the author acknowledges a combinatorics algorithm would be cleaner but opted for the nested loops given the absence of a suitable library utility.

The patterns flagged — complex_branching, deeply_nested, exit_heavy, god_function, long_function — are all consistent with what the source excerpt shows. A nesting depth of 6 comes directly from the five nested for loops plus the conditional guards inside them. A cyclomatic complexity of 25 reflects the number of paths through rotor validation, environment detection, and the combination search. A fan-out of 19 shows the function coordinates validation, machine construction, status reporting, and output assembly all in one place.

Max Nesting Depth 6
threshold: 4

This function was last changed 8 days ago. There are no bug-linked commits or reverts on the file. The god_function pattern is the primary concern: input validation, rotor/reflector parsing, Bombe machine construction, progress reporting, and result aggregation are all interleaved. Extracting the rotor-combination enumeration into a dedicated generator function — which yields each [rotor1, rotor2, rotor3, rotor4, reflector] tuple — would flatten the nesting immediately and make the search loop itself legible in isolation.


_addQPSoftLinebreaks — ToQuotedPrintable.mjs

_addQPSoftLinebreaks
src/core/operations/ToQuotedPrintable.mjs
17.09
fire
CC 23
ND 8
FO 7
touches/30d 1

This private method handles line-wrapping for Quoted-Printable MIME encoding — a format where non-ASCII bytes are percent-encoded and lines must not exceed 76 characters, with soft line breaks inserted as =\r\n. The source excerpt shows a while loop over the input string, with a cascade of regex-based conditions deciding where to truncate each line: hard CRLF, trailing newline, nearest preceding newline, nearest word boundary, incomplete encoding sequences, and multi-byte UTF-8 sequence boundaries.

The nesting depth of 8 is what makes this function stand out structurally. Each while iteration contains a chain of if/else if branches, and inside the final else branch there is a nested if guarding a second while loop that trims incomplete UTF-8 sequences — reaching 8 levels of nesting at its deepest. That inner loop tests a regex condition on each iteration and has its own break conditions, which is precisely the exit_heavy pattern: multiple paths out of nested control structures make it difficult to reason about which path was actually taken for a given input.

Max Nesting Depth 8
threshold: 4
Cyclomatic Complexity 23
threshold: 10

Quoted-Printable line-breaking is genuinely intricate — RFC 2045 has a non-trivial set of rules about padding characters, encoding boundaries, and whitespace — so some complexity is inherent. But a nesting depth of 8 means the logic for handling incomplete UTF-8 sequences is effectively invisible to anyone reading the outer loop. My recommendation is to extract the UTF-8 sequence trimming into a named helper (trimIncompleteUtf8Sequence or similar), which would reduce the nesting depth of the outer loop to roughly 5 and give the inner logic a clear, testable contract. With 23 independent paths, this function currently requires 23 distinct test cases for full coverage — extracting helpers would distribute that burden.


fromBase64 — Base64.mjs

fromBase64
src/core/lib/Base64.mjs
16.76
fire
CC 33
ND 4
FO 10
touches/30d 1

This is a library-level Base64 decoder that supports custom alphabets, optional padding, strict-mode validation, and configurable return types (string or byte array). The source excerpt shows the function handles a large number of cases explicitly: empty input, alphabet length validation, non-alphabet character stripping via dynamically constructed regex, strict-mode length and padding checks, and then the main decode loop with per-character index lookups and range-guarded output pushes.

The hub_function pattern is the most consequential flag here. fromBase64 sits in src/core/lib/Base64.mjs — a shared library module — meaning it is likely called from many operations across the codebase. A CC of 33 in a hub function means that every caller inherits the complexity of every execution path, including strict-mode validation branches and alphabet variants that a given caller may never exercise. Fan-out of 10 includes Utils.expandAlphRange, Utils.byteArrayToUtf8, and OperationError construction, all of which are called conditionally depending on the path taken.

Cyclomatic Complexity 33
threshold: 10

The external signals are worth noting here: there is 1 bug-linked commit on this file. That does not prove fromBase64 was the source of the bug, but it does mean the file has a prior quality-related event — and with 33 execution paths and hub-function coupling, this is the one function in the top five where I would prioritize adding a comprehensive test matrix before any refactoring. The strictMode branch is the most structurally isolated and would be a good candidate for extraction into a validateBase64Input helper, which would reduce the CC of the main decode loop and make the strict-mode rules independently auditable.


What else is in the picture

Beyond the top five, the context_only data shows a handful of functions worth a brief note. toHexFast in Hex.mjs, createNumArray in Arithmetic.mjs, and fromBinary in Binary.mjs are all in the “watch” quadrant — low structural complexity but recently active. They are not refactoring priorities, but their activity signals mean they are worth keeping an eye on as the codebase evolves. The two Vigenère cipher run functions (VigenèreDecode.mjs and VigenèreEncode.mjs) sit in the “debt” quadrant with zero touches in the last 30 days; they are structurally modest and currently dormant, so they sit at the bottom of any priority list.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
exit_heavy5
deeply_nested3
long_function3
god_function3
hub_function1

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

Reproduce This Analysis

git clone https://github.com/gchq/CyberChef
cd CyberChef
git checkout bd4c59cf34bfa4b1ae06839db70936786c5702df
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Related Analyses