mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-01 20:33:50 +00:00
docs: OpenThreat, one file a security tool serves about what it found in the open (#167)
* docs: OpenThreat, one file a security tool serves about what it found in the open Twelve rules that degrade and two that do not: a subject is public or it is not in the file (no private repos, no customer servers, no paid users' scans), and a secret is never located while it is open (rule, severity, subject, status only; no location, message or excerpt). Four kinds: finding, attack, indicator, advisory. Status open, fixed, mitigated, blocked, withdrawn; a withdrawn threat stays a while so directories retract it. Announcing is on by default with a one-switch opt-out in the tool's own settings. Discovery at /.well-known/openthreat.json, rel="openthreat", or a handed URL; origin is the verification. Mapped against SARIF, STIX 2.1 and CSAF rather than replacing them. First reporter: threatcrush.com/discovery (its own PR). First directory: nichedb.dev/c/threats (its own PR). Registered in DOC_SLUGS, NAV, STATIC_ROUTES and llms.txt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014cmNRtR2vL1p89dbVQ7FZJ * ci: trigger workflows --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
938bd5f632
commit
0fe1d423df
6 changed files with 375 additions and 0 deletions
175
docs/openthreat.md
Normal file
175
docs/openthreat.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# OpenThreat
|
||||
|
||||
OpenThreat is one file a security tool serves about what it found in the open: a finding in a public repository, an attack observed against the reporter's own infrastructure, an indicator worth blocking, an advisory. A directory reads the reporter's own file instead of a vendor's feed, a defender's agent reads it instead of a dozen dashboards, and the reporter stays the author of what it discloses and what it withholds. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface.
|
||||
|
||||
Status: **0.1**. A description of a file [ThreatCrush](https://threatcrush.com/discovery) serves and [nichedb.dev](https://nichedb.dev/c/threats) reads, published so any scanner or sensor can serve one and any directory can read it.
|
||||
|
||||
Slug: `openthreat`
|
||||
|
||||
## The problem
|
||||
|
||||
Every security tool finds things, and every one keeps what it found behind its own login. A scanner that runs on a thousand public repositories knows which rules fire most and where, and says nothing, because saying it would mean a feed, a schema, an API key and a sales call. The threat feeds that do exist are products: a licence per seat, terms that forbid redistribution, and a format each vendor invented. A defender wanting to know what is being found in the open, this week, by tools that are not theirs, cannot ask.
|
||||
|
||||
The tool already has the data, and for the open part of it there is no reason to hold it. What is missing is the one file that puts what a tool found in public where a reader can fetch it, with a rule for what may go in it.
|
||||
|
||||
## Terms
|
||||
|
||||
- A **reporter** is a tool or the operator of one: a scanner, a sensor, a daemon, a research team. Its **descriptor** is the file it serves.
|
||||
- A **threat** is one thing the reporter found: a finding, an attack, an indicator or an advisory.
|
||||
- A **subject** is what the threat is about: a public repository, a package, a host the reporter operates. A subject is never a private one.
|
||||
- A **directory** is anything that reads descriptors across reporters: a threat index, a dashboard, a defender's cache.
|
||||
|
||||
## The descriptor
|
||||
|
||||
A reporter serves a JSON document at `/.well-known/openthreat.json` on its own origin.
|
||||
|
||||
```json
|
||||
{
|
||||
"openthreat": "0.1",
|
||||
"reporter": {
|
||||
"name": "ThreatCrush",
|
||||
"web": "https://threatcrush.com",
|
||||
"operator": "https://profullstack.com/.well-known/openprofile.md",
|
||||
"tool": "threatcrush",
|
||||
"policy": "https://threatcrush.com/discovery#policy"
|
||||
},
|
||||
"updated": "2026-09-13T06:00:00Z",
|
||||
"threats": [
|
||||
{
|
||||
"id": "3f9a1c2b",
|
||||
"kind": "finding",
|
||||
"title": "SQL assembled by concatenation",
|
||||
"severity": "high",
|
||||
"confidence": "evidence",
|
||||
"rule": "js-sql-string-building",
|
||||
"cwe": "CWE-89",
|
||||
"category": "code",
|
||||
"subject": { "name": "northwind/api", "url": "https://github.com/northwind/api", "ref": "main", "commit": "9c1f0e2" },
|
||||
"location": { "file": "src/db/users.ts", "line": 42 },
|
||||
"status": "open",
|
||||
"first_seen": "2026-09-10T02:14:00Z",
|
||||
"last_seen": "2026-09-13T05:40:00Z",
|
||||
"message": "A query string is built from request input.",
|
||||
"consequence": "An attacker who controls the input controls the query.",
|
||||
"tlp": "clear"
|
||||
},
|
||||
{
|
||||
"id": "b71e0d44",
|
||||
"kind": "finding",
|
||||
"title": "Hardcoded credential",
|
||||
"severity": "critical",
|
||||
"rule": "secret-generic-credential",
|
||||
"cwe": "CWE-798",
|
||||
"category": "secret",
|
||||
"subject": { "name": "northwind/api", "url": "https://github.com/northwind/api" },
|
||||
"status": "open",
|
||||
"last_seen": "2026-09-13T05:40:00Z"
|
||||
},
|
||||
{
|
||||
"id": "ssh-91.232.105.3",
|
||||
"kind": "attack",
|
||||
"title": "SSH brute force",
|
||||
"severity": "medium",
|
||||
"rule": "ssh-bruteforce",
|
||||
"source": { "ip": "91.232.105.3", "country": "RU" },
|
||||
"target": { "port": 22, "service": "ssh" },
|
||||
"indicators": [{ "type": "ip", "value": "91.232.105.3" }],
|
||||
"status": "blocked",
|
||||
"first_seen": "2026-09-12T22:01:00Z",
|
||||
"last_seen": "2026-09-13T04:52:00Z",
|
||||
"count": 47
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The smallest valid descriptor is a reporter with a name and a threat with a title:
|
||||
|
||||
```json
|
||||
{ "reporter": { "name": "ThreatCrush" }, "threats": [{ "title": "SSH brute force" }] }
|
||||
```
|
||||
|
||||
The rules, and every one degrades except the two marked:
|
||||
|
||||
1. **`reporter.name` and `threats[].title` are the only required keys.** A reader lists what it was given and reports the rest as unstated.
|
||||
2. **`reporter`** is who found it. `web` is the reporter's site, `tool` the name of the software, `operator` the person or organisation answerable as an [OpenProfile.md](/openprofile) URL, and `policy` the page that says what the reporter publishes and what it withholds. A reporter with a policy page is one a reader can hold to it.
|
||||
3. **`updated`** on the descriptor is when anything in it last changed. A reader with it unchanged since its last fetch may skip the rest.
|
||||
4. **`id`** is stable for as long as the threat is the same thing. It is the dedupe key. Absent, the reader derives one from `kind`, `subject.name`, `rule` and `title`, and a retitled threat becomes a new one.
|
||||
5. **`kind`** is `finding` (something in a subject's code or configuration), `attack` (traffic observed against the reporter's own infrastructure), `indicator` (a value worth blocking or watching, on its own), or `advisory` (a statement about a vulnerability, with `refs`). Absent means `finding`.
|
||||
6. **`severity`** is `info`, `low`, `medium`, `high` or `critical`. **`confidence`** is the reporter's own word for how sure it is, kept as written. **`rule`** is the reporter's stable rule identifier, the same string a SARIF `ruleId` carries. **`cwe`** is `CWE-` and a number. **`category`** is the reporter's own grouping.
|
||||
7. **`subject`** is what the threat is about, and it is public by definition. `name` is the subject as the world knows it (`owner/repo`, a package name, a hostname the reporter operates), `url` where it lives, `ref` and `commit` what was looked at. **`location`** is `file` and `line` within the subject.
|
||||
8. **`status`** is `open`, `fixed`, `mitigated`, `blocked` or `withdrawn`. `fixed` is a finding no longer present; `blocked` is an attack the reporter stopped; `withdrawn` is a threat the reporter retracted, kept in the file for a while so directories learn it was. Absent means `open`.
|
||||
9. **`first_seen`**, **`last_seen`** are ISO 8601; **`count`** how many times it was observed between them. **`message`** says what was found and **`consequence`** what happens if it is real. **`refs`** are URLs: an advisory, a commit, a write-up.
|
||||
10. **`source`** and **`target`** describe an attack: where it came from (`ip`, `asn`, `country`) and what it hit (`port`, `service`, `path`). **`indicators`** are values a defender can act on, each `type` (`ip`, `cidr`, `domain`, `url`, `hash`, `ua`) and `value`.
|
||||
11. **`tlp`** is the Traffic Light Protocol label. A descriptor at `/.well-known/` is `clear` by definition, and a reader treats an absent `tlp` as `clear`. A reporter with anything else to say does not put it here.
|
||||
12. **Unknown keys are kept.** A reporter says more than this document names, and a reader passes it through under the reporter's own key.
|
||||
|
||||
**The two rules that do not degrade.**
|
||||
|
||||
**A subject is public or it is not in the file.** A finding about a private repository, a customer's server, a paying user's scan, or anyone's infrastructure but the reporter's own is not a threat in the open; it is someone's private security posture, and publishing it is a breach. A reporter serves only what was already visible to anyone who looked: public repositories, public packages, its own hosts. A reporter that scans private things keeps two tables and serves one.
|
||||
|
||||
**A secret is never located while it is open.** A finding whose category is `secret`, or that the reporter marks sensitive, is published with its `rule`, `severity`, `subject` and `status` only: no `location`, no `message`, no excerpt. The credential is already exposed by being in a public repository; the file must not be the map to it. Once `status` is `fixed` the location may follow.
|
||||
|
||||
Serve it as `application/json`. The descriptor is a claim; that it came from the reporter's own origin is the verification.
|
||||
|
||||
## Announcing, and opting out
|
||||
|
||||
A subject that is scanned by a reporter's tool did not ask to be listed. The reporter's policy says how a subject's owner turns publication off, and the reporter honours it: a subject that opts out disappears from the file on the next build, and any threat about it already read by a directory is served once more as `withdrawn` so the directory retracts it too. Announcing is on by default, because a finding in a public repository is public already and a list nobody is on lists nothing; opting out is one switch, in the tool's own settings, and costs nothing.
|
||||
|
||||
## Discovery
|
||||
|
||||
A reader finds a descriptor three ways, in this order:
|
||||
|
||||
1. `/.well-known/openthreat.json` on the reporter's origin.
|
||||
2. `<link rel="openthreat" href="...">` in the HTML of the reporter's home page, or a `Link: <...>; rel="openthreat"` header, when the file lives somewhere else.
|
||||
3. A URL handed to the reader directly.
|
||||
|
||||
A descriptor is **verified** when it was fetched from the same origin as `reporter.web`, or from `/.well-known/` on the origin the reader was pointed at. One found by the third route on some other host is a claim about the reporter by whoever hosts it, and a directory marks it so.
|
||||
|
||||
## Directories
|
||||
|
||||
A directory reading descriptors:
|
||||
|
||||
1. **Fetches hourly at least.** Threats are fixed, blocked and withdrawn on the hour; a file read once is a snapshot.
|
||||
2. **Dedupes on the reporter's origin and the threat's `id`.** A re-read updates the row; it never adds a second. A `withdrawn` threat is retracted, not merely marked.
|
||||
3. **Keeps the reporter's words and attributes the reporter.** Every listed threat says who found it and links the subject's `url`.
|
||||
4. **Never adds what the reporter withheld.** A directory that fetches the subject and finds the secret the reporter declined to locate, and publishes the line, has broken the second rule on the reporter's behalf.
|
||||
5. **Reports absence as absence.** No `status` is open. No `severity` is unstated, not low.
|
||||
|
||||
The first directory reading OpenThreat is the threats collection at [nichedb.dev](https://nichedb.dev/c/threats), which lists every reporter's threats as feeds, with RSS, JSON, an API and MCP over the same rows. The first reporter is [ThreatCrush](https://threatcrush.com/discovery), whose GitHub App scans public repositories and serves what it found.
|
||||
|
||||
## Related formats
|
||||
|
||||
- [SARIF](https://sarifweb.azurewebsites.net/) is what a scanner emits per run: `rule` here is SARIF's `ruleId`, and `location` its physical location. OpenThreat is the standing list across runs, with a subject and a status, and the disclosure rules SARIF does not have.
|
||||
- [STIX 2.1](https://oasis-open.github.io/cti-documentation/) describes threat intelligence exhaustively. An `indicator` here maps onto a STIX Indicator, an `attack` onto a Sighting. OpenThreat is the small file a tool can serve in an afternoon; a directory that speaks STIX can translate.
|
||||
- [CSAF](https://oasis-open.github.io/csaf/) is the vendor advisory format. An `advisory` here carries the CSAF document in `refs`.
|
||||
|
||||
## What is deliberately absent
|
||||
|
||||
**No private subjects.** Stated above and worth stating twice. The file is for what was found in the open.
|
||||
|
||||
**No exploit detail.** `message` says what was found; `consequence` says what it means. How to use it is nobody's business here.
|
||||
|
||||
**No scoring across reporters.** `severity` is the reporter's. A directory that ranks reporters or normalises severities labels the result as its own.
|
||||
|
||||
**No push.** A reporter serves a file. A directory that wants to be told may watch `updated`; a webhook is another document's business.
|
||||
|
||||
## Serving one
|
||||
|
||||
From the table the tool already keeps of what it found, filtered to public subjects, with secrets unlocated and opt-outs removed, at a fixed URL. ThreatCrush builds its file from the scans its GitHub App ran on public repositories, and nothing else.
|
||||
|
||||
## Related standards
|
||||
|
||||
- [OpenProfile.md](/openprofile): the `operator` behind a reporter.
|
||||
- [OpenServer](/docs/openserver), [OpenCoupon](/docs/opencoupon): the same serve-your-own-file shape for a provider's catalog and a merchant's promotions.
|
||||
- [OpenMCP](/openmcp): a directory that also serves its rows over MCP describes that door with an OpenMCP descriptor.
|
||||
|
||||
## Version history
|
||||
|
||||
| Version | Date | Change |
|
||||
|---|---|---|
|
||||
| 0.1 | 2026-09-13 | First publication: the descriptor, four kinds, twelve rules and the two that do not degrade, announcing and opting out, discovery, what a directory owes a reporter. |
|
||||
|
||||
## License
|
||||
|
||||
The specification text is CC BY 4.0. Serve it, copy it, extend it.
|
||||
Loading…
Add table
Add a link
Reference in a new issue