kevin/thecrewx

A framework beats twelve scripts

Twelve scripts that each work fine will still fail you together. What they lack is not features — it is shared conventions.

8 min read Architecture

Every security-minded person I know has the same folder. A script that checks for open ports. Another that reviews file permissions. One that diffs a config against a known-good copy. Each was written in an afternoon, each works, and together they are close to unusable.

Not because the code is bad. Because there are twelve different ideas about how configuration, logging, output, and failure should work.

Where the cost actually lands

The drift is not in the checks. It is in everything around them:

So you write a thirteenth script to run the other twelve, and it is the worst file in the folder — because it is entirely made of special cases.

What a core has to provide

Not features. Conventions:

  1. One configuration source, validated on load, failing clearly.
  2. One result shape that every check returns.
  3. One logger, structured by default.
  4. One failure contract — a check that cannot run returns a finding saying so.
  5. Severity as data, so reporting can sort without parsing strings.

That is the whole core. Everything genuinely interesting stays in the modules.

The result shape is the load-bearing decision

Get this right and reporting becomes trivial, because the reporter never needs to know what produced a finding:

@dataclass
class Finding:
    module: str
    severity: Severity
    title: str
    detail: str
    evidence: dict
    remediation: str | None = None

Two fields earn their place unexpectedly often. evidence holds the raw data behind the finding, so a reviewer can check the conclusion instead of trusting it. remediation is optional but changes how the report reads — a finding that tells you what to do next is worth several that only tell you something is wrong.

Failure has to be a finding

This is the change that mattered most in practice. A check that cannot complete — missing permission, unreachable host, absent file — returns a finding with an ERROR severity rather than raising.

The run completes. The report says explicitly which checks could not be performed. Compare that with a crash halfway through, which produces no report and no record of the eight checks that did pass.

An honest report with gaps beats a clean report that silently covered less than you assumed.

Keep the core boring

The strongest ongoing temptation is to add convenience to the core. A caching helper. A retry decorator. A tiny HTTP client, since three modules need one.

Every addition becomes something every module author must understand, and every one is a coupling point that makes modules harder to test alone. When I want a shared helper now, it goes in a utilities module that checks import like any other dependency — not into the core.

What I got wrong first

I let modules call each other. It seemed efficient: the port scanner already has data the service checker wants. What I actually built was an implicit dependency graph with an execution-order requirement nobody had written down, and two modules that could no longer be tested in isolation.

Now modules receive context and return findings. If two need the same data, the core gathers it once and passes it to both. Slightly more plumbing, dramatically less coupling.


This is the thinking behind aegiscore. The scripts folder still exists — it is just much smaller now.

All writing Next: Reading honeypot noise