@secureport/core
v2.3.0
Published
Shared domain model for Secureport. Imported by the hosted API the same way a third party would.
Maintainers
Readme
@secureport/core
The shared domain model for Secureport. The hosted API imports this package the same way a third party would — one model, one implementation.
MIT licensed. Zero runtime dependencies.
The model in one paragraph
A run produces findings. Findings are immutable evidence: one detection, in one run, with no status of its own. Each finding is fingerprinted and reconciled into an issue — the tracked record that persists across runs and carries status, severity, age and history. A snapshot is issue state as of a run, and is the only thing a report ever reads.
The tracked entity is the issue, not the finding. Two scanners reporting the same weakness produce one issue with two sources, not two rows; an issue that comes back after being fixed is a regression, not a discovery.
Install
npm install @secureport/coreWhat it does
Everything below is pure: no I/O, no clock, no randomness. Time and ids are arguments, which is what makes a report reproducible and a test a golden file.
import { buildSnapshot, importNuclei, renderMarkdown, parseSnapshot } from '@secureport/core';
const findings = importNuclei(text, { orgId, runId, targetId, now, newId });
const snapshot = buildSnapshot({
target,
run,
findings,
previous: parseSnapshot(lastTime), // optional — this is what makes a retest
now,
newId,
});
renderMarkdown(snapshot, { kind: 'retest', now });| | |
| ------------------ | -------------------------------------------------------------------------- |
| Importers | importNuclei, importZap, importBurp, importNessus, importGeneric |
| Fingerprint | fingerprint, vulnKey, normaliseLocation, FINGERPRINT_VERSION |
| Reconciliation | reconcile — the only thing that changes issue state |
| Migration | planRefingerprint — what a version bump would do, before it does it |
| Coverage | coversLocation — what makes auto-resolution safe |
| Snapshots | buildSnapshot, parseSnapshot |
| Reports | renderMarkdown, renderHtml, renderJson, buildReportModel |
Rendered examples of all five report kinds are at secureport.io — the source repository is private, so the site is where they live.
For a command line rather than an API, see
@secureport/cli:
npx @secureport/cli report pen scan.jsonl --url https://app.example.comStatus
Pre-1.0. Every export is a semver contract already, and the shapes are settled
by five report templates and five importers built on them — but below 1.0.0 a
minor may still move something. See the deprecation policy below for what
changes at 1.0.0.
0.2.x had a placeholder reconcile() that matched issues by title. It is gone
rather than deprecated — matching by title is not a simplified version of
matching by fingerprint, it is a different and wrong answer.
Severity
Five levels, ordered: critical · high · medium · low · advisory.
Compare them with severityRank, never as strings — sorting the strings puts
advisory first.
import { severityFromCvss, severityRank, SEVERITY_WEIGHTS } from '@secureport/core';
severityFromCvss(9.8); // 'critical' — CVSS v3.1 rating scale, unchanged
severityFromCvss(0); // 'advisory' — CVSS calls this "None"
severityRank('critical') > severityRank('high'); // true
SEVERITY_WEIGHTS.advisory; // 0 — advisories never inflate the exposure scoreseveritySource records why a severity was assigned — explicit, cvss,
engine_default or published_advisory, in that precedence order. A severity
with no provenance is not evidence.
published_advisory means a security bulletin said so. It is deliberately not
called advisory: that is the name of the lowest severity, and a critical
finding whose source read advisory was one word away from being skimmed past.
Remediation deadlines
sla_due_at derives from an issue's effective severity, and both helpers
are pure — neither reads the clock.
import { slaDueAt, slaStatus, DEFAULT_SLA_POLICY } from '@secureport/core';
const due = slaDueAt('high', issue.firstSeen); // 30 days after firstSeen
slaStatus(due, asOf); // 'within' | 'due_soon' | 'breached'Windows default to 7 / 30 / 90 / 180 days for critical through low, and
advisory has no deadline at all — a zero-weight severity should not
manufacture breaches. Pass your own SlaPolicy to override; the defaults are
this package's opinion, not the domain's.
Deadlines run from firstSeen, which is never reset — so a regression does not
give a year-old problem a fresh clock.
Reports
Every report template consumes a Snapshot and nothing else: no database
handle, no network, no clock. That is what makes a report reproducible, testable
against golden files, and renderable by someone with findings on disk and no
account.
The snapshot is the artefact; a PDF is a view of it.
Public contracts
Every export is a semver contract. Two of them are worth stating separately, because they outlive any one version of this package.
fingerprint_version
An issue's identity is the pair (fingerprintVersion, fingerprint), not the
fingerprint alone. FINGERPRINT_VERSION is fp_v2 and it travels with every
fingerprint everywhere it is stored, so a fingerprint written today can always
be interpreted — including by code that has moved on.
fingerprint = sha256(targetId + '|' + vulnKey + '|' + normalisedLocation + '|' + (parameter ?? port ?? ''))vulnKey is the engine-independent name for a weakness: a mapping-table entry
where one exists, then the CWE, then the category, then engine:ruleId. That
chain is what makes two scanners reporting the same weakness one issue rather
than two.
Changing any of this changes the version, and a version change means a
migration that recomputes fingerprints while preserving history — firstSeen
is never reset — not a silent recomputation that orphans every issue. That is
the whole reason the version exists, and it is why it is a contract rather than
an implementation detail.
The JSON report
renderJson emits a JsonReport, versioned by reportVersion (currently
1), for consumers rendering their own view. It carries the derived answers
beside the snapshot rather than only the snapshot, and two of its fields are
there for a specific reason:
basis— how the findings were produced, as data. A consumer must not be able to present automated results as manual testing simply by not reading the prose.limitations— what the report does not establish, as data, for the same reason.
reportVersion bumps when the shape changes incompatibly. Adding a field does
not bump it, so read defensively.
Deprecation
Nothing that works stops working inside a major version, and nothing is removed
without having been deprecated in a release you could have seen: marked
@deprecated with its replacement named, left working for at least one further
minor and 90 days, then removed in a major.
| | | | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Patch | Behaviour unchanged, or a fix to behaviour that was already wrong. Never a signature change | | Minor | Additions. New exports, new optional fields, new accepted input. Adding a field to a returned object is a minor, so read defensively | | Major | Anything that could break a build or change an answer — a removed or renamed export, a narrowed input, a changed default |
Making an optional field required, or a required field optional, is a breaking
change. Anything not reachable from this package's entry point is internal,
whatever it looks like; reaching into dist/ is not covered.
fingerprint_version never changes without a migration. That is what the
version is for: a change obliges a per-org re-fingerprint that preserves
history — firstSeen is never reset — rather than a silent recomputation that
orphans every issue.
A published version is immutable. A mistake is corrected by publishing another one, not by changing or unpublishing that one.
If the only correct fix for a vulnerability is a breaking change, we make it and say so. A policy that would rather leave a hole open than break a signature is not one worth having.
