From e3ea44aa905df12ca09963f29626fb96d83ade4f Mon Sep 17 00:00:00 2001 From: Ben Fellows Date: Sun, 2 Aug 2026 11:13:31 +1200 Subject: [PATCH 1/6] Start user-docs/ with the architecture page, and design the site around it D-10 chose plain Markdown with the renderer deferred. This tests that choice against the hardest page in the plan rather than the easiest, and it holds: CommonMark plus tables carried the whole thing, and the requirement list is Markdown with tables, static assets, and a theme whose image CSS is one rule away from correct. Nothing here argues for a framework. Two bugs only appear once a diagram is a standalone .svg rather than inline HTML, and both fail silently as a broken image: - HTML named entities are undefined in XML. A standalone .svg is parsed as XML, so — makes the file fail to render entirely. All numeric now. - Angle brackets in the SVG's CSS, even inside a comment, parse as tags. The diagrams were re-authored at a 900px viewBox with larger type and zones stacked vertically instead of side by side. Carried over at 1160px they scaled to 7.5px body text in a docs column, which is unreadable; at 900px they hold above 10px down to an 820px viewport. That withdraws the wide-layout variant this design first proposed for explainer pages -- it was compensating for over-wide diagrams, and the site keeps one layout instead of two. Both defects found in the re-authored set were geometric and invisible in review: a 4px card overlap and a text run escaping its card. Checking every getBBox() against its containing rectangle found both, and that check is written into user-docs/README.md as a convention. The IA maps 34 pages across 8 sections against what already exists -- 12 are a porting job, 5 are scattered, 17 need writing. It also proposes one amendment to D-10, for the decisions log: user-docs owns evaluate as well as use, because a reviewer approving TOPS for an AWS org reads before anything is installed and will not read a README to do it. Co-Authored-By: Claude Opus 5 --- design/architecture/README.md | 20 + design/architecture/aws-integration.html | 1107 +++++++++++++++++ design/docs-site/information-architecture.md | 318 +++++ design/docs-site/wireframes.html | 565 +++++++++ user-docs/README.md | 75 ++ .../diagrams/aws-child-account-linking.svg | 123 ++ .../assets/diagrams/aws-docker-runtime.svg | 136 ++ .../diagrams/aws-parent-account-setup.svg | 122 ++ .../start-here/how-tops-connects-to-aws.md | 167 +++ 9 files changed, 2633 insertions(+) create mode 100644 design/architecture/README.md create mode 100644 design/architecture/aws-integration.html create mode 100644 design/docs-site/information-architecture.md create mode 100644 design/docs-site/wireframes.html create mode 100644 user-docs/README.md create mode 100644 user-docs/assets/diagrams/aws-child-account-linking.svg create mode 100644 user-docs/assets/diagrams/aws-docker-runtime.svg create mode 100644 user-docs/assets/diagrams/aws-parent-account-setup.svg create mode 100644 user-docs/start-here/how-tops-connects-to-aws.md diff --git a/design/architecture/README.md b/design/architecture/README.md new file mode 100644 index 0000000..da0ba08 --- /dev/null +++ b/design/architecture/README.md @@ -0,0 +1,20 @@ +# Architecture explainer — design exploration + +`aws-integration.html` is the standalone page that was built first, to work out how to +explain the AWS integration to a security reviewer. It did its job twice over: it settled the +structure of the explainer archetype, and writing it is what surfaced the open SNS topic now +tracked as [N-11](../../docs/roadmap.md#n-11--lock-down-the-account-linking-sns-topic). + +**It is not the canonical version.** That is +[`user-docs/start-here/how-tops-connects-to-aws.md`](../../user-docs/start-here/how-tops-connects-to-aws.md), +which is what publishes to docs.teemops.com. If the two disagree, the docs page wins. + +The difference that matters: this page embeds its diagrams inline at a 1160px viewBox, sized +for a full-bleed layout. The docs page uses standalone 900px SVGs in +`user-docs/assets/diagrams/`, re-authored so the text survives being scaled into a docs +column. See the sample-test findings in +[`design/docs-site/information-architecture.md`](../docs-site/information-architecture.md). + +Kept because the exploration is worth having on record, not because it needs maintaining. +**Do not edit it to track product changes** — edit the docs page. This one can be deleted +whenever it stops being interesting. diff --git a/design/architecture/aws-integration.html b/design/architecture/aws-integration.html new file mode 100644 index 0000000..5ebeda0 --- /dev/null +++ b/design/architecture/aws-integration.html @@ -0,0 +1,1107 @@ +How TOPS connects to AWS + + +
+ +
+
TOPS · self-hosted AWS security scanning · architecture
+

How TOPS connects to AWS

+

+ TOPS runs on your infrastructure and reads your AWS accounts through cross-account IAM roles. + There are no stored access keys, no inbound network path to the install, and no data path to us. + This page shows exactly what gets created, who creates it, and what crosses each boundary. +

+
+ v0.4.0 + Docker Compose + Laravel 11 + Vue + CloudFormation · SNS · SQS · STS + Apache-2.0, source-available +
+
+ +
+
+
Your infrastructure
+

The server you run TOPS on. Owns the database, the findings, and the only copy of the encrypted role ARNs.

+
+
+
Your parent AWS account
+

One account you nominate. Holds the messaging resources — an SNS topic, SQS queues, one S3 bucket. Deployed once.

+
+
+
Child accounts
+

Every account you want scanned. Each gets one CloudFormation stack and one IAM role. Add as many as you have.

+
+
+ + +
+
STAGE
01
+
+

Standing up the parent account

+

Run once · ./install.sh --aws · one region

+

+ The installer runs as a throwaway container on your own machine, using your own AWS CLI credentials. + It deploys two CloudFormation stacks into an account you choose, then writes the resulting queue names and + ARNs back to disk. Nothing here is shared with other installs — these are your resources, in your account. +

+ + + +
    +
  1. Deploy. The installer checks sts:get-caller-identity, confirms the account and region with you, then runs sam deploy.
  2. +
  3. Create. CloudFormation creates the topic, the queues, the dead-letter queue and the bucket. You own all of it.
  4. +
  5. Subscribe. teemops-sns is subscribed to teemops_main, so a notification becomes a queue message.
  6. +
  7. Report back. Stack outputs are written to generated/teemops.env and loaded by the containers on restart.
  8. +
+ +

+ Skippable. TOPS runs without this step — you just cannot link an account yet. Come back later with + ./install.sh --aws-only. Everything above can be deleted by removing two CloudFormation stacks. +

+
+
+ + +
+
STAGE
02
+
+

Connecting child accounts, one link at a time

+

Repeat per account · no limit · no per-account infrastructure in the parent

+

+ TOPS never asks for credentials to a child account. It hands the account's own administrator a + CloudFormation quick-create link, pre-filled with an ExternalId generated for that account alone. + The administrator creates the stack in their own console, under their own session. The stack calls home once + to say which role ARN was created — and that is the whole handshake. +

+ + + +
    +
  1. Hand over a link. A console quick-create URL carrying four parameters: your parent account id, the region, the account's ExternalId and its UniqueId.
  2. +
  3. The admin creates the stack. In their account, with their permissions. TOPS has no session there and cannot create it for them.
  4. +
  5. The stack calls home. A custom resource publishes the new role ARN, external id and unique id to your SNS topic, which fans into teemops_main.
  6. +
  7. Your worker picks it up. Outbound long-poll from inside your network. The message is accepted only if both ids match a record you already created.
  8. +
  9. The stack completes. TOPS replies to the CloudFormation ResponseURL; the admin sees CREATE_COMPLETE, or a failure with a reason.
  10. +
+ +

+ Why there is no account limit. Linking adds one stack in the child account and one database row in yours — + no queue, no topic, no bucket, no per-account resource in the parent. The tenth account and the thousandth cost the + same to connect. Deleting the stack fires the same custom resource in reverse and unlinks it. +

+
+
+ + +
+
STAGE
03
+
+

What actually runs: six containers

+

One docker-compose.yml · one host · docker compose up -d

+

+ TOPS is a Laravel monolith and a MySQL database, split across containers by role rather than by service boundary. + The web tier never talks to AWS. The worker tier does — outbound only, assuming a role per scan and holding the + credentials for the life of one job. Scan jobs travel on the database queue, so a scan needs no AWS messaging at all. +

+ + + +
    +
  1. The UI enqueues, it does not scan. Starting a scan writes a job row. The web tier holds no AWS credentials at all.
  2. +
  3. Workers poll the database. Scan jobs use the database queue driver, with SELECT … FOR UPDATE SKIP LOCKED so five workers never collide.
  4. +
  5. Account linking polls SQS. The only pool that needs AWS messaging, and it stays switched off until TOPS_SQS_ARN is set.
  6. +
  7. Scanning assumes the role. One AssumeRole per job, temporary credentials, read calls, results written home.
  8. +
+ +

+ Air-gapped from us by construction. The parent account, the queues, the database and the findings are all yours. + There is no telemetry endpoint, no license check, and no hosted control plane in this path — the container image is + the only thing you pull from outside, and you can build it yourself with install-build.sh. +

+
+
+ + +
+

What crosses the boundary

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectionTrafficCarriesWhen
outboundHTTPS to AWS API endpoints, signed with credentials from sts:AssumeRoleDescribe / List / Get calls against the account being scannedDuring a scan
outboundSQS ReceiveMessage / DeleteMessage long-pollNothing — an empty poll until a stack is createdContinuously, if AWS messaging is set up
into your AWSOne sns:Publish from the child account's CloudFormation stackRole ARN, external id, unique idOnce per account, at link and at unlink
neverInbound connections to the TOPS hostThere is no listener. AWS is polled, not called back.
neverLong-lived AWS access keysNone are requested, entered, or stored. Only role ARNs, encrypted.
neverFindings, resource inventory, or account metadata leaving your installScan output is written to your MySQL and stays there
+
+
+ + +
+

What the child-account role can actually do

+

+ Worth being precise about, because this is the first question every security reviewer asks. The role is + not read-only. Scanning uses only the read half; the write half exists for the operational features + that provision and manage AWS security services on your behalf. +

+
+
+
Readused by every scan
+
    +
  • ReadOnlyAccess (AWS managed)
  • +
  • ResourceGroupsandTagEditorReadOnlyAccess
  • +
+

This is the whole permission set the scanner exercises. If you only want scanning, this is all you need.

+
+
+
Writeonly for the ops features
+
    +
  • securityhub:* · guardduty:* · macie:* · inspector:*
  • +
  • config:Put* / Delete* — rules, recorders, delivery channels, conformance packs
  • +
  • cloudtrail: create, update, delete, start/stop logging
  • +
  • logs: create/delete log groups, streams, metric filters
  • +
  • cloudformation: create, update, delete, change sets
  • +
  • s3:CreateBucket / DeleteBucket (for Config delivery)
  • +
  • iam:PassRole → arn:aws:iam::*:role/tops*
  • +
+

+ The template is a plain CloudFormation file in your own S3 bucket. Strip the policies you do not want before + you publish the link — the scanner keeps working, the ops features stop. +

+
+
+
+ + +
+

The shape of it, in numbers

+
+
+
2
+
stacks in the parent
+

Deployed once. Delete them and the integration is gone.

+
+
+
1
+
stack per child account
+

Created by that account's own admin. Revocable by them at any time.

+
+
+
0
+
stored AWS keys
+

Cross-account roles with a per-account external id, assumed per job.

+
+
+
0
+
inbound ports to AWS
+

Every connection is opened from inside your network.

+
+
+
~153
+
region jobs per full scan
+

Fanned across five worker processes by default.

+
+
+
6
+
containers on one host
+

app, worker, mysql, backup, maildev, and a restore container that stays parked.

+
+
+
+ +
+
TOPS v0.4.0 — self-hosted AWS security posture scanning. Architecture as built, not as planned.
+
Templates: templates/iam.role.child.account.cfn.yaml · Parent stacks: infra/cloud-stack/ · Runtime: docker-compose.yml
+
+ +
diff --git a/design/docs-site/information-architecture.md b/design/docs-site/information-architecture.md new file mode 100644 index 0000000..b42cf88 --- /dev/null +++ b/design/docs-site/information-architecture.md @@ -0,0 +1,318 @@ +# docs.teemops.com — information architecture and content map + +Design work, not built. Scope: the structure and content plan for the user documentation +site, plus the page design for **How TOPS connects to AWS**, which is the page that prompted +it. + +**Out of scope for this worktree:** `teemops.com` marketing site wireframes and design. That +continues elsewhere. Where this document touches the marketing site it is only to draw the +boundary between the two. + +## What D-10 already decided + +[D-10](../../docs/roadmap.md#d-10-in-full-where-user-documentation-lives) settled most of the +structural questions. Nothing here relitigates them: + +| Decision | Consequence for this design | +| --- | --- | +| A new top-level `user-docs/`, not `docs/` | `docs/` stays a contributor knowledge base in an engineering voice. Nothing in this plan moves it | +| One site, structured **by task**, not by audience | No "developer track" / "business track" split. Depth increases *down the page* instead | +| `docs.teemops.com`, Cloudflare Pages | Already the plan for the marketing site; this is the same pattern on a second directory | +| **Plain Markdown; renderer decided later** | This design deliberately does **not** pick a static-site generator. See [Tooling](#tooling-stays-deferred-on-purpose) | +| `README.md` stays canonical for install | The site links to it rather than copying it. Duplication is the thing D-10 was most concerned about | + +### One place this design extends D-10 + +D-10 scoped `user-docs/` to pick up **from "you're logged in"** — findings, remediation, team +management, scan profiles — with `README.md` owning everything before that. + +That holds for *operators*. It does not hold for the audience that prompted this work. +**A CISO deciding whether to permit TOPS in their AWS organisation reads before anything is +installed, and will not read a GitHub README to do it.** They want trust boundaries, IAM +permissions, and what crosses the wire. That is a third audience D-10 did not model. + +**Proposed amendment, for the decisions log:** `user-docs/` owns **evaluate** and **use**. +`README.md` stays canonical for the *commands* — the one-liner, the flags, the upgrade steps +— and the site links out to it rather than restating them. This keeps D-10's +anti-duplication rule intact while admitting the evaluator. Suggest recording as **D-13** +before the first page is written, so it isn't rediscovered. + +## Audiences + +Three, in the order they arrive. The task-based structure serves all three without splitting +the site, because the tasks themselves are ordered this way. + +| | Reads | Arrives from | Leaves when | +| --- | --- | --- | --- | +| **Evaluator** — CISO, CTO, security reviewer | Architecture, trust boundaries, IAM permissions, licence | A vendor-review request, or the marketing site | They can answer "is this safe to allow in our org?" | +| **Operator** — DevOps, platform engineer | Install, connect accounts, upgrade, back up, troubleshoot | GitHub, or the evaluator handing it over | TOPS is running and scanning | +| **User** — whoever reads the findings | Findings, remediation, insights, scan profiles, team | The app itself, usually mid-task | They know what a finding means and what to do about it | + +The evaluator is the smallest audience and the highest-stakes: they are a gate, not a user. +One unanswered question on their list stops the other two ever arriving. + +## Site map + +``` +docs.teemops.com +│ +├── Start here +│ ├── What TOPS is — and what it is not +│ ├── How TOPS connects to AWS ← evaluator entry point +│ ├── What it costs to run +│ ├── Install → links README, does not copy it +│ ├── Connect your first AWS account +│ └── Run your first scan +│ +├── Using TOPS +│ ├── The dashboard +│ ├── Running a scan +│ ├── Scan profiles and scan types +│ ├── Reading a finding +│ ├── Resolving a finding +│ └── Insights +│ +├── AWS accounts +│ ├── Adding an account +│ ├── What the IAM role can do ← the honest permissions page +│ ├── Removing an account +│ └── Many accounts at once +│ +├── Your organization +│ ├── Organizations and data isolation +│ ├── Inviting and managing members +│ ├── Roles and permissions +│ └── Your profile +│ +├── Running the service +│ ├── Configuration reference +│ ├── Upgrading and rolling back +│ ├── Backups and restore +│ ├── Workers, queues and scan speed +│ ├── Troubleshooting +│ └── Uninstalling +│ +├── Security +│ ├── The security model +│ ├── Where your data lives +│ ├── Reporting a vulnerability +│ └── Licence and trademark +│ +└── Reference + ├── Glossary + ├── What TOPS scans — services and rules + ├── Command reference + └── Release notes +``` + +Eight sections is at the top of what a sidebar carries before it needs search. If it grows, +**Security** folds into **Start here** before anything else does — its pages are read once, +by one audience, in one sitting. + +## Content inventory + +Status is about the *content*, not the file. "Exists" means the substance is written +somewhere and needs porting and a change of voice, not that a `user-docs/` page is ready. + +| Page | For | Status | Source | Priority | +| --- | --- | --- | --- | :---: | +| **Start here** | +| What TOPS is — and what it is not | All | ✍️ Write | `README.md` intro, roadmap Direction | 1 | +| How TOPS connects to AWS | Evaluator | ✅ **Built this session** | `design/architecture/aws-integration.html` | 1 | +| What it costs to run | Evaluator, Operator | ⚠️ Partial | `install.sh` `aws_intro()`, `docker-compose.README.md` | 2 | +| Install | Operator | ✅ Exists | `README.md` § Install — link, don't copy | 1 | +| Connect your first AWS account | Operator | ✅ Exists | `README.md` § Scanning a real AWS account | 1 | +| Run your first scan | Operator, User | ⚠️ Thin | `README.md`, `docs/features/scan-*.md` | 1 | +| **Using TOPS** | +| The dashboard | User | ✍️ Write | `Pages/Dashboard.vue` | 3 | +| Running a scan | User | ✍️ Write | `Pages/Scans/*`, `NewScanModal.vue` | 2 | +| Scan profiles and scan types | User | ✍️ Write | `ScanProfilesService`, `ScanTypesService`, `config/scan.php` | 2 | +| Reading a finding | User | ✍️ Write | `Pages/Findings/*`, `docs/features/filter-findings-by-*.md` | **1** | +| Resolving a finding | User | ⚠️ Partial | Rule remediation text (N-4 gave all 74 rules one) | **1** | +| Insights | User | ✍️ Write | `Pages/Insights/`, `docs/features/insights-by-service.md` | 3 | +| **AWS accounts** | +| Adding an account | Operator | ✅ Exists | `docs/features/onboarding-flow.md` (rewrite — it is a spec, not a guide) | 2 | +| What the IAM role can do | Evaluator | ✅ **Built this session** | `templates/iam.role.child.account.cfn.yaml` | **1** | +| Removing an account | Operator | ✍️ Write | Stack deletion → `Delete` custom resource | 3 | +| Many accounts at once | Operator, Evaluator | ✍️ Write | This session's research | 3 | +| **Your organization** | +| Organizations and data isolation | Evaluator, User | ✍️ Write | `OrganizationPermission`, multi-tenancy practice | 2 | +| Inviting and managing members | User | ✍️ Write | `Pages/Organizations/*`, `organization-team-management-plan.md` | 2 | +| Roles and permissions | User | ⚠️ Unstable | `OrganizationPermission` — see [#—/X-5](../../docs/roadmap.md), reconciliation open | 3 | +| Your profile | User | ✍️ Write | `Pages/Profile/` | 4 | +| **Running the service** | +| Configuration reference | Operator | ⚠️ Scattered | `.env.docker.example`, `docker-compose.yml` comments | 2 | +| Upgrading and rolling back | Operator | ✅ Exists | `README.md` § Upgrading | 2 | +| Backups and restore | Operator | ✅ Exists, strong | `docs/backups.md` | 2 | +| Workers, queues and scan speed | Operator | ✅ Exists | `worker-supervisord.conf` comments, `parallel-region-scans.md` | 3 | +| Troubleshooting | Operator | ✅ Exists | `DEBUG.md`, `README.md` § If something goes wrong | 2 | +| Uninstalling | Operator, Evaluator | ⚠️ Partial | `docker-compose.README.md`; the two CFN stacks | 3 | +| **Security** | +| The security model | Evaluator | ✅ **Built this session** | The architecture page's boundary table | **1** | +| Where your data lives | Evaluator | ✍️ Write | Nothing leaves the install — needs stating plainly | 2 | +| Reporting a vulnerability | Evaluator | ✍️ Write | No `SECURITY.md` exists — **gap** | **1** | +| Licence and trademark | Evaluator | ✅ Exists | `LICENSE`, `TRADEMARK.md`, roadmap D-7 | 3 | +| **Reference** | +| Glossary | All | ✅ Exists | `docs/GLOSSARY.md` | 3 | +| What TOPS scans — services and rules | Evaluator, User | ✅ Exists | `docs/planning.md`, the rule JSON | 2 | +| Command reference | Operator | ✍️ Write | `artisan` commands, `backup.sh`, `install.sh` flags | 4 | +| Release notes | All | ✅ Exists | `CHANGELOG.md` | 4 | + +**Totals:** 34 pages — 12 exist in substance, 5 partial or scattered, 17 to write from +scratch. Roughly a third of the site is a porting-and-rewriting job rather than an authoring +job, which is the argument for doing the IA before the writing. + +### Two gaps this inventory exposed + +1. **There is no `SECURITY.md`.** A public repo with a `security` label, three open security + issues and a CISO-facing architecture page has nowhere to report a vulnerability. That is + a repo-hygiene fix, not a docs-site fix, and it should not wait for the site. +2. **`docs/quick-start.md` is dead.** It describes a Nuxt 3 + Serverless + Prisma monorepo — + the pre-pivot stack. It is linked from `docs/README.md` and would mislead anyone who found + it. Delete it rather than port it. + +## What not to document yet + +Per `docs/practices/product.md` — no page for a feature that does not exist. Verified against +`docs/PROGRESS.md`, which is checked against the code: + +| | Why not | +| --- | --- | +| **Firebase / OAuth sign-in** | Built but **off by default** (`FIREBASE_USER_AUTH=false`). Documenting it as a path implies the default install needs a Firebase project. It does not. One line under Configuration reference, no page | +| **MFA** | Partially built, deliberately gated, and not reachable without Firebase yet | +| **Report export, scheduled scans, multi-cloud** | Not started. No controller, no route, no UI | + +## Page archetypes + +Three, which is enough. Every page in the map is one of them. + +**1. Task page** — the default. Title, one-sentence outcome, prerequisites as a short list, +numbered steps with real command output, then a "when it goes wrong" section. Depth increases +down the page: the first screen is enough for a non-technical reader, the last third is for +someone debugging. + +**2. Explainer** — architecture, security model, permissions. Diagram first, prose second, +a table of specifics last. This is the archetype **How TOPS connects to AWS** already +implements: zoned diagrams, numbered captions, a boundary table, and an honest +read/write permissions split. + +**3. Reference** — glossary, commands, rules, release notes. Scannable, tabular, no +narrative. Assumed to be arrived at from search or a link, never read top to bottom. + +## Sample test: the architecture page as a real Markdown page + +Built and measured, not theorised. `user-docs/start-here/how-tops-connects-to-aws.md` plus +three standalone SVGs in `user-docs/assets/diagrams/`, rendered through a deliberately +minimal shell (python-markdown + ~60 lines of CSS — no framework, which was the point). + +**Verdict: Markdown-first survives. The diagrams need one theme rule and some rework.** + +### What worked + +Plain CommonMark carried the whole page — headings, tables, lists, blockquote callout, +linked images. Nothing needed MDX, a component, or a build step. The three diagrams are +external `.svg` files referenced with `![alt](path.svg)`, which keeps the Markdown readable +and the diagrams independently editable. Each SVG carries its own `prefers-color-scheme` +block, so dark mode works without inheriting page tokens. + +### Two bugs that only appear once the SVG is standalone + +Both would have shipped invisibly if the diagrams had stayed inline in HTML. + +1. **HTML named entities are undefined in XML.** `—`, `×`, `→` are fine in + inline SVG inside an HTML document, because the HTML parser resolves them. A standalone + `.svg` served as `image/svg+xml` is parsed as XML, where only five entities exist — the + file fails to render **entirely**, not partially. All must be numeric references + (`—`). Worth a lint rule if more diagrams follow. +2. **`` inside a CSS comment breaks the parse.** An XML parser reads it as a tag. + Angle brackets cannot appear anywhere in a standalone SVG's CSS, even in comments. + +Validate every diagram with an XML parser before committing. A malformed SVG shows as a +broken image, which reads as a missing file rather than a syntax error. + +### The real finding: diagram width versus prose measure + +The diagrams were first authored at a 1160px viewBox, carried over from a full-bleed HTML +page. That does not survive a docs column. Measured effective body-text size after scaling: + +| Diagram viewBox | Layout | Diagram width | Scale | Body text | Readable | +| --- | --- | --- | --- | --- | --- | +| 1160px | Prose column (68ch), default | 686px | 0.59 | 6.8px | No | +| 1160px | Full content column | 757px | 0.65 | 7.5px | No | +| 1160px | Wide variant, 1680px viewport | 1202px | 1.04 | 11.9px | Yes, but only above ~1600px | +| **900px** | **Standard column, 1440px viewport** | **892px** | **0.99** | **12.4px** | **Yes** | +| **900px** | **Standard column, 1100px viewport** | **767px** | **0.85** | **10.7px** | **Yes** | +| **900px** | **Standard column, 820px viewport** | **765px** | **0.85** | **10.6px** | **Yes** | + +**The diagrams were re-authored at a 900px viewBox** with proportionally larger type (body +12.5px, mono 13px, titles 15.5px) and zones stacked vertically rather than placed side by +side, which is what pays for the larger text. They now read at every desktop width in the +*standard* layout. + +Two rules follow, and they are the whole theme requirement: + +- **Images must not inherit the prose measure.** Markdown wraps an image in a `

`, so + `p { max-width: 68ch }` silently caps every diagram at 686px. One rule fixes it + (`main p:has(> img) { max-width: none }`) and it is the single most important line of CSS + on the site. +- **The theme must collapse its side rails at the usual breakpoints** — on-page TOC below + ~1200px, sidebar below ~900px. Every candidate renderer already does this by default. + +**The earlier recommendation of a wide layout variant for explainer pages is withdrawn.** It +was compensating for over-wide diagrams. With the diagrams sized correctly, the standard +layout is enough, and the site keeps one layout instead of two. + +Below ~800px — phones — no diagram of this density is readable inline, and no amount of +layout work changes that. Every diagram is therefore wrapped in a link to itself, +`[![alt](x.svg)](x.svg)`, plain CommonMark, so one tap opens the standalone SVG and it scales +to the viewport. One line, and it is the right answer on small screens regardless. + +### Guardrails for the next diagram + +Hand-placed SVG at this density does not survive being eyeballed. Both defects found in the +re-authored set were geometric — a 4px card overlap and a text run escaping its card — and +both were caught by loading the SVGs in a browser and comparing every `getBBox()` against its +containing rectangle. Worth keeping as a check when diagrams are added: + +- no text extends past the card that contains it, or past its zone +- no two non-decorative cards overlap +- nothing sits outside the viewBox + +Numbered chips that sit on a boundary-crossing arrow will trip a naive version of that check; +exclude them by class rather than loosening the tolerance. + +### What this settles for tooling + +The page needs: Markdown with tables, static assets served alongside, per-page layout +selection, and a theme whose image CSS is one rule away from correct. That is the whole +requirement list, and every MkDocs- or Docsify-class renderer meets it. **Nothing found here +argues for a framework.** + +## Tooling stays deferred, on purpose + +D-10 said plain Markdown, renderer chosen once there is content. **This design does not +change that**, and the wireframes should not be read as a vote for a framework. + +What the wireframes commit to is only what any renderer in the MkDocs/Docsify class gives +you free: a left sidebar of sections, a content column at a readable measure, an on-page +table of contents, and search. Nothing in the IA needs MDX, React components, or a build +step. The one page with real layout demands — the architecture explainer — is already a +self-contained HTML page with inline SVG, which every candidate renderer can embed. + +**Revisit when:** the first slice is written and the section count or search need actually +bites. Not before. + +## First slice + +D-10's own first slice, sharpened by the inventory above. Ordered by what unblocks the most. + +1. `SECURITY.md` in the repo root — not a site page, and not blocked by any of this. +2. `user-docs/` with the four priority-1 evaluator/user pages: **How TOPS connects to AWS**, + **What the IAM role can do**, **Reading a finding**, **Resolving a finding**. +3. Cloudflare Pages connected, `docs.teemops.com` resolving. +4. `README.md`'s setup section trimmed to a summary that links in, so install has one + canonical copy. +5. Delete `docs/quick-start.md`. + +After that, D-10's rule holds: a page per feature as it ships, written by whoever ships it. +No documentation sprint. diff --git a/design/docs-site/wireframes.html b/design/docs-site/wireframes.html new file mode 100644 index 0000000..04da769 --- /dev/null +++ b/design/docs-site/wireframes.html @@ -0,0 +1,565 @@ +docs.teemops.com — structure and wireframes + + +

+ +
+
TOPS · user documentation · structure and wireframes
+

docs.teemops.com

+

+ One site, structured by task, serving three audiences without splitting into tracks. + This is the shell, the three page archetypes every page fits, and the full content map — + 34 pages, of which 12 already exist in substance somewhere in the repo. +

+
+ D-10: user-docs/, not docs/ + Cloudflare Pages + Plain Markdown — no framework yet + Marketing site out of scope +
+
+ + +
+
01 · The shell
+

What every page sits inside

+

+ Sidebar of sections, content at a readable measure, on-page contents on the right. Nothing + here needs a framework — it is what any renderer in the MkDocs or Docsify class gives you + out of the box, which is what keeps D-10's "decide the tooling later" honest. + Shown with the page that prompted the site. +

+ +
+
+
+
+
docs.teemops.com/start-here/how-tops-connects-to-aws
+
+
+ + + +
+
Start here / How TOPS connects to AWS
+

How TOPS connects to AWS

+
Written for security reviewers
+

+ TOPS runs on your infrastructure and reads your AWS accounts through cross-account IAM + roles. No stored access keys, no inbound network path, no data path to us. +

+ +
+
Stage 1 — Standing up the parent account
+
zoned SVG · installer → CloudFormation → SNS / SQS / S3
+
+ +
+ +
+
Stage 2 — Connecting child accounts
+
quick-create URL → IAM role → custom resource → your worker
+
+ +
+ +
+
DirectionTrafficCarries
+
outboundAWS API, signed by AssumeRoleRead calls during a scan
+
neverInbound connectionsThere is no listener
+
neverLong-lived access keysRole ARNs only
+
+
+ + + +
+
+
+ +

+ The audience chip is the one non-obvious element. D-10 ruled out splitting the site into + audience tracks, and that is right — but a CISO landing here from a vendor review needs to know + in one glance that this page was written for them, not that they have wandered into an operator + runbook. One chip does that without forking the navigation. +

+
+ + +
+
02 · Page archetypes
+

Three shapes, and every page is one of them

+

+ Constraining to three is what keeps a docs site consistent when pages are written by whoever + shipped the feature, months apart. Depth increases down the page in all three — that is how + one site serves a non-technical reader and an engineer without a track switch. +

+ +
+ +
+
+
Archetype 1 — the default
+
Task page
+
Someone is trying to do a thing right now. Get them to done, then help them when it breaks.
+
+
+
H1 — the task, as a verb
+
One sentence: what you will have at the end
+
Before you start — short list
+
Numbered steps, with real output
+
Numbered steps, with real output
+
When it goes wrong — symptom → cause → fix
+
Next: the page they will need after this
+
+
18 of 34 pages. Install, connect an account, invite a member, restore a backup.
+
+ +
+
+
Archetype 2 — the gate
+
Explainer
+
Someone is deciding whether to trust this. Diagram first, prose second, specifics last.
+
+
+
H1 + audience chip
+
Thesis: the claim, in two sentences
+
Diagram — zoned, with numbered captions
+
Prose: what the diagram does not say
+
Table of specifics — the honest one
+
What we do not claim
+
+
6 pages. How TOPS connects to AWS, the security model, what the IAM role can do.
+
+ +
+
+
Archetype 3 — the lookup
+
Reference
+
Arrived at from search or a link, never read top to bottom. Scannable, tabular, no narrative.
+
+
+
H1 + one line of scope
+
Jump links / filter
+
Table, sorted the way it is looked up
+
Table, sorted the way it is looked up
+
Table, sorted the way it is looked up
+
+
10 pages. Glossary, what TOPS scans, command reference, configuration, release notes.
+
+ +
+
+ + +
+
03 · Content map
+

All 34 pages, and what already exists

+

+ Status is about the content, not the file — "have it" means the substance is written somewhere + in the repo and needs porting and a change of voice, not that a page is ready. Roughly a third of + this site is a rewriting job rather than an authoring job, which is the argument for mapping it + before writing any of it. +

+ +
+ Have it — port and rewrite for the reader + Partial or scattered across files + Write from scratch +
+ +
+ +
+
Start here 6
+
What TOPS is — and what it is not
+
How TOPS connects to AWS
+
What it costs to run
+
Install — links README, does not copy
+
Connect your first AWS account
+
Run your first scan
+
+ +
+
Using TOPS 6
+
The dashboard
+
Running a scan
+
Scan profiles and scan types
+
Reading a finding
+
Resolving a finding
+
Insights
+
+ +
+
AWS accounts 4
+
Adding an account
+
What the IAM role can do
+
Removing an account
+
Many accounts at once
+
+ +
+
Your organization 4
+
Organizations and data isolation
+
Inviting and managing members
+
Roles and permissions
+
Your profile
+
+ +
+
Running the service 6
+
Configuration reference
+
Upgrading and rolling back
+
Backups and restore
+
Workers, queues and scan speed
+
Troubleshooting
+
Uninstalling
+
+ +
+
Security 4
+
The security model
+
Where your data lives
+
Reporting a vulnerability
+
Licence and trademark
+
+ +
+
Reference 4
+
Glossary
+
What TOPS scans — services and rules
+
Command reference
+
Release notes
+
+ +
+
Not yet
+
Firebase / OAuth sign-in — off by default
+
MFA — partial, deliberately gated
+
Report export — not started
+
Scheduled scans — not started
+
Azure, GCP — AWS only
+
+ +
+ +

+ Bold pages are the first slice. Four of them, chosen because they unblock the most: the two + evaluator pages are already written as of this session, and the two finding pages are what a design + partner hits within minutes of their first scan. Everything else waits for the feature that needs it. +

+
+ + +
+
04 · What the map exposed
+

Two gaps that are not docs problems

+ +
+
+
34
+
pages in the map
+

Eight sections. At the point where a sidebar still works without search.

+
+
+
12
+
already written
+

Substance exists in the repo. Needs porting and a change of voice, not authoring.

+
+
+
5
+
partial or scattered
+

True but spread across compose comments, install output and feature specs.

+
+
+
17
+
to write
+

Mostly "Using TOPS" — the half of the product no document currently describes.

+
+
+ +

+ There is no SECURITY.md. A public repo with three open security issues and a CISO-facing + architecture page has nowhere to report a vulnerability. That is repo hygiene, not a docs-site + page, and it should not wait for the site to be built. +

+ +

+ docs/quick-start.md is dead. It describes a Nuxt 3 + Serverless + Prisma monorepo + — the pre-pivot stack — and it is linked from docs/README.md. Anyone who finds it is + actively misled. Delete it rather than port it. +

+
+ +
+
Design only — nothing here is built. Full content inventory, audiences and the proposed D-10 amendment: design/docs-site/information-architecture.md.
+
Out of scope: teemops.com marketing site wireframes and design, which continue in a separate worktree.
+
+ +
diff --git a/user-docs/README.md b/user-docs/README.md new file mode 100644 index 0000000..345a24e --- /dev/null +++ b/user-docs/README.md @@ -0,0 +1,75 @@ +# TOPS user documentation + +The source for **docs.teemops.com** — documentation for the people *running* TOPS, as opposed +to `docs/`, which is the contributor and maintainer knowledge base. See +[D-10](../docs/roadmap.md#d-10-in-full-where-user-documentation-lives) for why the two are +kept apart. + +**Status: first slice.** One page so far. The structure it will grow into is designed in +[`design/docs-site/information-architecture.md`](../design/docs-site/information-architecture.md) +— 34 pages across 8 sections, with a content map showing what already exists elsewhere in +the repo and needs porting rather than writing. + +## What is here + +| Page | For | +| --- | --- | +| [How TOPS connects to AWS](start-here/how-tops-connects-to-aws.md) | Security reviewers approving TOPS for an AWS organisation | + +## Conventions + +**Plain Markdown, no framework.** The renderer is deliberately undecided until there is +enough content to need navigation and search. Nothing here may depend on renderer-specific +syntax — no MDX, no components, no shortcodes. If a page cannot be written in CommonMark plus +tables, that is a signal about the page, not about the tooling. + +**Install instructions live in `README.md`, not here.** Linking beats copying; two canonical +copies drift. Pages reference it rather than restating commands. + +**Do not document what does not exist.** Verified against `docs/PROGRESS.md`. Firebase +sign-in is built but off by default, MFA is gated, and report export, scheduled scans and +multi-cloud are not started — none get a page. + +### Diagrams + +Standalone SVG files in `assets/diagrams/`, referenced as linked images so one click opens +them full size: + +```markdown +[![Description of what the diagram shows](../assets/diagrams/name.svg)](../assets/diagrams/name.svg) +``` + +**Author at a 900px viewBox**, with body text at 12.5px, mono at 13px and titles at 15.5px. +That is what keeps a diagram readable when it is scaled into a docs column — at 900px it +renders near 1:1 on a laptop and stays above 10px down to an 820px viewport. Wider diagrams +do not survive: the same content at a 1160px viewBox scaled to 7.5px body text and was +unreadable. Stack zones vertically rather than side by side; that is what buys the room for +larger type. + +Four rules, learned the hard way when the first three diagrams were extracted from inline +HTML: + +1. **Numeric character references only.** `—`, not `—`. A standalone `.svg` is + parsed as XML, where HTML named entities are undefined and the file fails to render + entirely. +2. **No angle brackets anywhere in the SVG's CSS**, including inside comments — an XML parser + reads them as tags. +3. **Each SVG carries its own theme.** An image-embedded SVG cannot inherit page tokens, so + every diagram needs its own `@media (prefers-color-scheme: dark)` block. +4. **Check the geometry, do not eyeball it.** Both defects in the first set were a card + overlapping another card and a text run escaping its card — invisible in review, obvious + to a script that compares every `getBBox()` against its containing rectangle. + +Validate before committing — a malformed SVG shows as a broken image, which looks like a +missing file rather than a syntax error: + +```bash +python3 -c "import xml.etree.ElementTree as ET,sys; [ET.parse(f) for f in sys.argv[1:]]; print('ok')" user-docs/assets/diagrams/*.svg +``` + +### Writing a new page + +Pick one of the three archetypes in the IA document — **task**, **explainer**, or +**reference** — and follow its shape. Depth increases down the page: the first screen serves +a non-technical reader, the last third serves someone debugging. That is what lets one site +serve both without splitting into audience tracks. diff --git a/user-docs/assets/diagrams/aws-child-account-linking.svg b/user-docs/assets/diagrams/aws-child-account-linking.svg new file mode 100644 index 0000000..fb54b91 --- /dev/null +++ b/user-docs/assets/diagrams/aws-child-account-linking.svg @@ -0,0 +1,123 @@ + + Linking a child AWS account to TOPS + TOPS generates a CloudFormation quick-create URL. The child account's own administrator creates a stack that makes a cross-account IAM role plus a custom resource. The custom resource publishes the role ARN to your parent SNS topic, your worker long-polls SQS, matches it against a pending record and stores it, then replies to CloudFormation. + + + + + + + + + + + + Your TOPS install + + + Add AWS account + Creates a pending record, then + external_id · random + + + aws:process-sqs + Matches both ids, then stores + the role ARN in your database. + + + Any account you want scanned ×N + + + + + CloudFormation stack + tops-vendor-audit + Created by that account’s own admin, in their own console session. + + + IAM role + Trusts your parent account, + and only with sts:ExternalId + + + Custom resource + Fires at stack create, and + again at delete · sns:Publish + + + Your parent account + + + SQS · teemops_main + Long-polled from inside your + own network. + + + SNS · teemops-sns + Messaging only. No credentials, + no findings, no customer data. + + + + 1 + + + 2 + + + + 3 + + + + 4 + + + 5 + + The link is the only thing that travels outward. What comes back is one message, on one topic, that you own. + diff --git a/user-docs/assets/diagrams/aws-docker-runtime.svg b/user-docs/assets/diagrams/aws-docker-runtime.svg new file mode 100644 index 0000000..b43b43d --- /dev/null +++ b/user-docs/assets/diagrams/aws-docker-runtime.svg @@ -0,0 +1,136 @@ + + TOPS Docker Compose runtime and scan fan-out + Six containers on one host: app, mysql, worker, backup, db-restore and maildev. The worker's supervisord pools poll the database queue for scan jobs and SQS for account linking, and reach AWS outbound only, through sts:AssumeRole. + + + + + + + + + + + + One Docker Compose project + + + app + teem/tops:<tag> + nginx + php-fpm on :8080 + No AWS credentials at all + + + mysql 8.0 + volume: mysql-data + Orgs, accounts, findings + — and the job queue + + + worker + same image, different entrypoint + supervisord pools + tops-queue ×1 + default, teemops_audit — orchestrates a scan + tops-region-queue ×5 + teemops_audit_region — does the scanning + tops-account-queue · tops-scheduler + aws:process-sqs, off unless SQS is configured + + + backup + Full 17:00 · diff hourly + ~/.tops/backups + + + Also in the project + maildev on :8090 + db-restore, parked behind a profile + + + AWS — outbound only + + + SQS long-poll + teemops_main + Account linking only. + + + sts:AssumeRole + Role ARN + external id. + Temporary credentials, + held for one job only. + + + + + Child accounts ×N + Describe / List / Get + across every enabled + region. Results come + back to your database. + + + + 1 + + + 2 + + + 3 + + + 4 + + + + One full scan of one account + 1 orchestrator job → ~153 region jobs → 5 workers in parallel + + Nothing listens for AWS. Every arrow leaving this box was started from inside it. + diff --git a/user-docs/assets/diagrams/aws-parent-account-setup.svg b/user-docs/assets/diagrams/aws-parent-account-setup.svg new file mode 100644 index 0000000..bf00129 --- /dev/null +++ b/user-docs/assets/diagrams/aws-parent-account-setup.svg @@ -0,0 +1,122 @@ + + TOPS parent account setup + The installer container on your own machine deploys two CloudFormation stacks into an AWS account you choose, creating an SNS topic, four SQS queues and an S3 bucket, then writes the stack outputs back to generated/teemops.env. + + + + + + + + + + + + Your machine + + + Installer container + ./install.sh --aws + One-shot. Uses your own AWS CLI credentials. + + + Written back to disk + generated/teemops.env + Queue names, ARNs and the template URL. + + + Your parent AWS account + + + CloudFormation + teemops-core-docker + teemops-messaging + Two stacks, one region. + Re-runnable. + + + SNS topic + teemops-sns + The one inbound path. + + + SQS queues + teemops_main + teemops_main_dlq + teemops_audit + teemops_audit_region + + + S3 bucket + <env>-<id>-tops-deploy + Hosts the child-account + template. Public read on + /templates/* only. + + + Why four queues + teemops_main carries the + account-link callbacks, and + _dlq catches them after five + failures. The audit pair is + for SQS-mode scanning: idle. + + + + 1 + + + 2 + + + 3 + + + 4 + + Two stacks, one region, one account. Idle when you are not scanning. + diff --git a/user-docs/start-here/how-tops-connects-to-aws.md b/user-docs/start-here/how-tops-connects-to-aws.md new file mode 100644 index 0000000..66b8256 --- /dev/null +++ b/user-docs/start-here/how-tops-connects-to-aws.md @@ -0,0 +1,167 @@ +# How TOPS connects to AWS + +> **Written for security reviewers.** If you have been asked to approve TOPS for your AWS +> organisation, this page is the whole answer. No prior knowledge of TOPS is assumed. + +TOPS runs on your infrastructure and reads your AWS accounts through cross-account IAM roles. +There are no stored access keys, no inbound network path to the install, and no data path to +us. This page shows exactly what gets created, who creates it, and what crosses each boundary. + +The diagrams use three zones throughout: + +| Zone | What it is | +| --- | --- | +| **Your infrastructure** | The server you run TOPS on. Owns the database, the findings, and the only copy of the role ARNs | +| **Your parent AWS account** | One account you nominate. Holds an SNS topic, SQS queues and one S3 bucket. Deployed once | +| **Child accounts** | Every account you want scanned. Each gets one CloudFormation stack and one IAM role | + +--- + +## Stage 1 — Standing up the parent account + +*Run once · `./install.sh --aws` · one region* + +The installer runs as a throwaway container on your own machine, using your own AWS CLI +credentials. It deploys two CloudFormation stacks into an account you choose, then writes the +resulting queue names and ARNs back to disk. Nothing here is shared with other installs. + +[![Diagram: the installer container on your machine deploys two CloudFormation stacks into your parent AWS account, creating an SNS topic, four SQS queues and an S3 bucket, then writes the outputs back to generated/teemops.env.](../assets/diagrams/aws-parent-account-setup.svg)](../assets/diagrams/aws-parent-account-setup.svg) + +1. **Deploy.** The installer checks `sts:get-caller-identity`, confirms the account and region + with you, then runs `sam deploy`. +2. **Create.** CloudFormation creates the topic, the queues, the dead-letter queue and the + bucket. You own all of it. +3. **Subscribe.** `teemops-sns` is subscribed to `teemops_main`, so a notification becomes a + queue message. +4. **Report back.** Stack outputs are written to `generated/teemops.env` and loaded by the + containers on restart. + +Two of the four queues — `teemops_audit` and `teemops_audit_region` — are created but stay +idle. Scans run on the database queue by default; those two exist for operators who want to +move scanning onto SQS. + +**This step is optional.** TOPS runs without it — you just cannot link an account yet. You can +come back later with `./install.sh --aws-only`. Everything above is removed by deleting two +CloudFormation stacks. + +--- + +## Stage 2 — Connecting child accounts + +*Repeat per account · no limit · no per-account infrastructure in the parent* + +TOPS never asks for credentials to a child account. It hands the account's own administrator a +CloudFormation quick-create link, pre-filled with an `ExternalId` generated for that account +alone. The administrator creates the stack in their own console, under their own session. + +[![Diagram: TOPS generates a CloudFormation quick-create URL; the child account admin creates a stack that makes a cross-account IAM role plus a custom resource; the custom resource publishes the role ARN to the parent SNS topic; the TOPS worker long-polls SQS, matches it and stores it, then replies to CloudFormation.](../assets/diagrams/aws-child-account-linking.svg)](../assets/diagrams/aws-child-account-linking.svg) + +1. **Hand over a link.** A console quick-create URL carrying four parameters: your parent + account id, the region, the account's `ExternalId` and its `UniqueId`. +2. **The admin creates the stack.** In their account, with their permissions. TOPS has no + session there and cannot create it for them. +3. **The stack calls home.** A custom resource publishes the new role ARN, external id and + unique id to your SNS topic, which fans into `teemops_main`. +4. **Your worker picks it up.** Outbound long-poll from inside your network. The message is + accepted only if both ids match a record you already created. +5. **The stack completes.** TOPS replies to the CloudFormation `ResponseURL`; the admin sees + `CREATE_COMPLETE`, or a failure with a reason. + +**Why there is no account limit.** Linking adds one stack in the child account and one database +row in yours — no queue, no topic, no bucket, no per-account resource in the parent. The tenth +account and the thousandth cost the same to connect. Deleting the stack fires the same custom +resource in reverse and unlinks it. + +--- + +## Stage 3 — What actually runs + +*One `docker-compose.yml` · one host · `docker compose up -d`* + +TOPS is a Laravel monolith and a MySQL database, split across containers by role. The web tier +never talks to AWS. The worker tier does — outbound only, assuming a role per scan and holding +the credentials for the life of one job. + +[![Diagram: the Docker Compose project runs app, mysql, worker, backup, db-restore and maildev containers. The worker's supervisord pools poll the database queue for scan jobs and SQS for account linking, and reach AWS through sts:AssumeRole to make read calls against each child account.](../assets/diagrams/aws-docker-runtime.svg)](../assets/diagrams/aws-docker-runtime.svg) + +1. **The UI enqueues, it does not scan.** Starting a scan writes a job row. The web tier holds + no AWS credentials at all. +2. **Workers poll the database.** Scan jobs use the database queue driver, with + `SELECT … FOR UPDATE SKIP LOCKED` so five workers never collide. +3. **Account linking polls SQS.** The only pool that needs AWS messaging, and it stays switched + off until `TOPS_SQS_ARN` is set. +4. **Scanning assumes the role.** One `AssumeRole` per job, temporary credentials, read calls, + results written home. + +A full scan of one account fans out to roughly 153 region jobs across five worker processes. +Raise `TOPS_WORKER_PROCESSES` only with the memory and the AWS API rate limits to match. + +--- + +## What crosses the boundary + +| Direction | Traffic | Carries | When | +| --- | --- | --- | --- | +| **Outbound** | HTTPS to AWS API endpoints, signed with credentials from `sts:AssumeRole` | Describe / List / Get calls against the account being scanned | During a scan | +| **Outbound** | SQS `ReceiveMessage` / `DeleteMessage` long-poll | Nothing — an empty poll until a stack is created | Continuously, if AWS messaging is set up | +| **Into your AWS** | One `sns:Publish` from the child account's CloudFormation stack | Role ARN, external id, unique id | Once per account, at link and at unlink | +| **Never** | Inbound connections to the TOPS host | There is no listener. AWS is polled, not called back | — | +| **Never** | Long-lived AWS access keys | None are requested, entered, or stored | — | +| **Never** | Findings, inventory or account metadata leaving your install | Scan output is written to your MySQL and stays there | — | + +There is no telemetry endpoint, no licence check, and no hosted control plane in this path. The +container image is the only thing you pull from outside, and you can build it yourself with +`install-build.sh`. + +--- + +## What the child-account role can actually do + +Worth being precise about, because it is the first question every security reviewer asks. **The +role is not read-only.** Scanning uses only the read half; the write half exists for the +operational features that manage AWS security services on your behalf. + +**Read — used by every scan** + +- `ReadOnlyAccess` (AWS managed) +- `ResourceGroupsandTagEditorReadOnlyAccess` + +This is the whole permission set the scanner exercises. + +**Write — only for the operational features** + +- `securityhub:*`, `guardduty:*`, `macie:*`, `inspector:*` +- `config:Put*` / `Delete*` — rules, recorders, delivery channels, conformance packs +- `cloudtrail:` create, update, delete, start/stop logging +- `logs:` create/delete log groups, streams, metric filters +- `cloudformation:` create, update, delete, change sets +- `s3:CreateBucket` / `DeleteBucket` (for AWS Config delivery) +- `iam:PassRole` → `arn:aws:iam::*:role/tops*` + +**If you only want scanning, remove the write policies.** The template is a plain +CloudFormation file in your own S3 bucket. Strip the policies you do not want before you +publish the link — the scanner keeps working, the operational features stop. + +--- + +## The shape of it, in numbers + +| | | +| --- | --- | +| **2** stacks in the parent | Deployed once. Delete them and the integration is gone | +| **1** stack per child account | Created by that account's own admin. Revocable by them at any time | +| **0** stored AWS keys | Cross-account roles with a per-account external id, assumed per job | +| **0** inbound ports | Every connection is opened from inside your network | +| **~153** region jobs per full scan | Fanned across five worker processes by default | +| **6** containers on one host | app, worker, mysql, backup, maildev, and a restore container that stays parked | + +--- + +## Related + +- [What the IAM role can do](../aws-accounts/what-the-iam-role-can-do.md) — the same permissions, policy by policy +- [The security model](../security/the-security-model.md) — trust boundaries in full +- [Install](../start-here/install.md) — when you are ready to run it + +*Source of truth for this page: `templates/iam.role.child.account.cfn.yaml`, +`infra/cloud-stack/`, and `docker-compose.yml` in the TOPS repository.* From 987e6db51cea9e49515ae57539bd09ee1cd248a9 Mon Sep 17 00:00:00 2001 From: Ben Fellows Date: Sun, 2 Aug 2026 11:17:41 +1200 Subject: [PATCH 2/6] Add the archetype templates, house voice rules, and a writing order The IA said three archetypes and left it there, which is not enough to keep 17 pages written by different people months apart from diverging. Each archetype is now a copyable skeleton with the guidance inline as comments that do not render, so the rule is next to the thing it governs rather than in a document nobody rereads. The seven voice rules are the ones that are checkable rather than tasteful. Two earn their place from work already done here: "never claim more than the code does" comes from the child IAM role not being read-only, which is the kind of overstatement a reviewer catches and then stops trusting the rest of the site over -- so every explainer carries a "What we do not claim" section. "Symptom before cause" is because troubleshooting is reached by searching an error message, not by browsing a subsystem. The writing order is dependency order, not importance order. Reading a finding precedes resolving one, because resolution is meaningless to someone who cannot yet interpret severity. Deploying the site waits until four real pages exist, since a live site with one page invites filler. Also fixes three dead links the architecture page shipped with -- it pointed at pages the IA plans but nobody has written. They now point at the repository files that exist today, and a link check is written into the conventions so the next page cannot repeat it. Co-Authored-By: Claude Opus 5 --- design/docs-site/archetypes/explainer.md | 100 ++++++++++++++++++ design/docs-site/archetypes/reference.md | 73 +++++++++++++ design/docs-site/archetypes/task.md | 92 ++++++++++++++++ design/docs-site/information-architecture.md | 62 +++++++++++ user-docs/README.md | 44 +++++++- .../start-here/how-tops-connects-to-aws.md | 6 +- 6 files changed, 370 insertions(+), 7 deletions(-) create mode 100644 design/docs-site/archetypes/explainer.md create mode 100644 design/docs-site/archetypes/reference.md create mode 100644 design/docs-site/archetypes/task.md diff --git a/design/docs-site/archetypes/explainer.md b/design/docs-site/archetypes/explainer.md new file mode 100644 index 0000000..357e908 --- /dev/null +++ b/design/docs-site/archetypes/explainer.md @@ -0,0 +1,100 @@ + + +# Where your data lives + +> **Written for security reviewers.** If you have been asked to approve TOPS, this page is +> the whole answer. No prior knowledge of TOPS is assumed. + + + +Every finding TOPS produces is written to a MySQL database on a server you run. Nothing is +sent anywhere else. This page shows where each category of data is stored, who can read it, +and what happens to it when you remove an account or uninstall. + + + +![Description of what the diagram shows, written as a full sentence for screen readers and for anyone the image fails to load for.](../assets/diagrams/name.svg) + + + +## What the diagram does not say + +Prose here — the qualifications, the edge cases, the "except when". The diagram carries the +shape; this carries the caveats that would clutter it. + + + +## The specifics + +| Data | Stored in | Readable by | Removed when | +| --- | --- | --- | --- | +| Findings | Your MySQL, `findings` table | Members of the owning organisation | The account is unlinked | +| IAM role ARNs | Your MySQL, `aws_accounts` | Members of the owning organisation | The account is unlinked | +| Scan logs | Container stdout, your host | Anyone with shell on the host | Your log rotation says so | + + + +## What we do not claim + +- TOPS does not encrypt findings at the application layer. Encryption at rest is delegated + to the host — see [D-9](https://github.com/teemops/tops/blob/develop/docs/roadmap.md). +- Anyone with shell access to your host can read the database. There is no protection + against your own operators. + + + +## Related + +- [How TOPS connects to AWS](../start-here/how-tops-connects-to-aws.md) +- [Backups and restore](../running-the-service/backups-and-restore.md) + +*Source of truth for this page: `path/to/file` in the TOPS repository.* + + diff --git a/design/docs-site/archetypes/reference.md b/design/docs-site/archetypes/reference.md new file mode 100644 index 0000000..d58db5e --- /dev/null +++ b/design/docs-site/archetypes/reference.md @@ -0,0 +1,73 @@ + + +# Configuration reference + +Every environment variable TOPS reads, what it does, and what happens if you leave it unset. +Set these in `.env` in your install directory, then restart with `docker compose up -d`. + + + +## Jump to + +[Application](#application) · [Database](#database) · [AWS](#aws) · [Workers](#workers) · [Backups](#backups) + + + +## Application + +| Variable | Default | What it does | +| --- | --- | --- | +| `APP_URL` | `http://localhost:8080` | The URL TOPS builds links with. Set it to the address people actually browse to, or emailed links break | +| `APP_KEY` | generated at install | Laravel's encryption key. Rotate with `php artisan key:generate`; everyone is logged out | +| `APP_DEBUG` | `true` | Leave `false` anywhere reachable by someone you do not trust — debug pages expose configuration | + +## AWS + +| Variable | Default | What it does | +| --- | --- | --- | +| `TOPS_DEPLOYMENT_REGION` | *unset* | The region the parent stacks were deployed into. Set by the AWS install step | +| `TOPS_SQS_ARN` | *unset* | The account-linking queue. **While unset, account linking is switched off** and the worker pool does not start | +| `TOPS_WORKER_PROCESSES` | `5` | Region workers in parallel. Each holds ~60–120 MB and one MySQL connection | + + + +## Related + +- [Upgrading and rolling back](upgrading-and-rolling-back.md) +- [Workers, queues and scan speed](workers-queues-and-scan-speed.md) — before raising `TOPS_WORKER_PROCESSES` + + diff --git a/design/docs-site/archetypes/task.md b/design/docs-site/archetypes/task.md new file mode 100644 index 0000000..880de32 --- /dev/null +++ b/design/docs-site/archetypes/task.md @@ -0,0 +1,92 @@ + + +# Connect an AWS account + + + +Links one AWS account to TOPS so it can be scanned. Takes about five minutes, and most of +that is waiting for CloudFormation. + + + +## Before you start + +- TOPS is installed and you can sign in +- You ran the AWS step during install — if you skipped it, run `./install.sh --aws-only` first +- You can create a CloudFormation stack in the account you want to scan, or you know who can + + + +## Steps + +1. **Open AWS accounts and choose Add account.** TOPS creates a pending record and + generates a link that is specific to this account. + +2. **Send the link to whoever administers the AWS account**, or open it yourself if that + is you. It opens the CloudFormation quick-create page, pre-filled. + + The link carries an `ExternalId` generated for this account alone. Treat it like a + password — anyone with it can complete this link. + +3. **Create the stack.** The defaults are correct. Acknowledge the IAM capability + checkbox — the stack creates a role, so AWS requires it. + + ``` + Stack name: tops-vendor-audit + Status: CREATE_IN_PROGRESS → CREATE_COMPLETE + ``` + +4. **Wait for TOPS to pick it up.** Usually a few seconds after the stack completes. The + account moves from *Pending* to *Active* on its own — you do not need to refresh or + paste anything back. + + + +## When it goes wrong + +**The stack sits at CREATE_IN_PROGRESS for more than a few minutes.** +TOPS never received the notification, so CloudFormation is waiting for a reply that is not +coming. Check that the AWS step ran during install (`TOPS_SQS_ARN` is set) and that the +account-linking worker is running: `docker compose logs worker | grep account-queue`. + +**The stack fails with "Account not found".** +The link was generated for a different TOPS organisation, or the pending record was +deleted. Generate a fresh link and create the stack again. + +**The account shows Active but scans return nothing.** +The role exists but cannot be assumed. Confirm the account is not in a region your +credentials cannot reach, and see [Troubleshooting](../running-the-service/troubleshooting.md). + + + +## Next + +- [Run your first scan](run-your-first-scan.md) +- [What the IAM role can do](../aws-accounts/what-the-iam-role-can-do.md) — if you were asked to justify the permissions + + diff --git a/design/docs-site/information-architecture.md b/design/docs-site/information-architecture.md index b42cf88..25b148e 100644 --- a/design/docs-site/information-architecture.md +++ b/design/docs-site/information-architecture.md @@ -199,6 +199,68 @@ read/write permissions split. **3. Reference** — glossary, commands, rules, release notes. Scannable, tabular, no narrative. Assumed to be arrived at from search or a link, never read top to bottom. +Copyable skeletons, with the guidance inline as comments that do not render: +[`archetypes/task.md`](archetypes/task.md) · +[`archetypes/explainer.md`](archetypes/explainer.md) · +[`archetypes/reference.md`](archetypes/reference.md). + +## House voice + +D-10's decision to structure by task rather than by audience only works if every page is +written the same way. Pages will be written by whoever ships the feature, months apart, so +the rules have to be few enough to remember and specific enough to check. + +**Depth increases down the page.** The first screen serves someone non-technical; the last +third serves someone debugging. This is the mechanism that lets one site serve three +audiences without splitting into tracks — if a page cannot be arranged this way, it is +probably two pages. + +**Write from the reader's side of the screen.** They manage *accounts* and *findings*, not +`aws_accounts` rows and rule evaluations. Name things the way the UI names them. Internal +vocabulary belongs in `docs/`, not here. + +**Symptom before cause.** Troubleshooting entries are reached by searching an error message, +so lead with the literal text the reader has in front of them — then the cause, then the fix. +A section organised by subsystem is unfindable by the person who needs it. + +**Show real output.** Readers compare their screen to the page character by character. +Paraphrased output silently breaks that. + +**Never claim more than the code does.** The child IAM role is not read-only, so no page says +it is. A reader who catches one overstatement stops trusting the rest of the site, and +reviewers are exactly the audience that checks. Every explainer carries a *What we do not +claim* section for this reason. + +**Link, do not restate.** Install commands live in `README.md`; the site links to them. Two +canonical copies drift, and the stale one is the one someone follows. + +**No page for a feature that does not exist.** Verified against `docs/PROGRESS.md`, not +against the roadmap — the roadmap describes intent, `PROGRESS.md` describes the code. + +## Writing order + +Priority in the inventory says what matters; this says what unblocks what. Roughly dependency +order, not importance order. + +1. **`SECURITY.md`** — repo hygiene, blocked by nothing, and the one gap with an active + reason to exist today. +2. **The two evaluator pages** — already written. *How TOPS connects to AWS* is done; *What + the IAM role can do* is a straight extraction from it and the child template. +3. **Reading a finding**, then **Resolving a finding.** In that order: resolution is + meaningless to someone who cannot yet interpret severity and evidence. These two are what + a design partner hits within minutes of a first scan, and nothing currently describes them. +4. **Deploy the site.** Cloudflare Pages, `docs.teemops.com`. Do it once there are four real + pages rather than before — a live site with one page invites filler. +5. **Trim `README.md`'s setup section** to link inward. Only after an install page exists to + link *to*; doing it earlier points at nothing. +6. **Delete `docs/quick-start.md`.** +7. **Everything else, as the feature that needs it ships.** No documentation sprint. + +The one ordering trap: **Configuration reference** looks cheap because the values already +exist in `.env.docker.example` and the compose file. It is not — it is a second copy of +something the code states, and it goes stale silently. Write it late, and only after deciding +whether it can be generated. + ## Sample test: the architecture page as a real Markdown page Built and measured, not theorised. `user-docs/start-here/how-tops-connects-to-aws.md` plus diff --git a/user-docs/README.md b/user-docs/README.md index 345a24e..23c69e9 100644 --- a/user-docs/README.md +++ b/user-docs/README.md @@ -26,6 +26,24 @@ tables, that is a signal about the page, not about the tooling. **Install instructions live in `README.md`, not here.** Linking beats copying; two canonical copies drift. Pages reference it rather than restating commands. +**No links to pages that do not exist yet.** The IA maps 34 pages; only some are written. A +page may link forward only once the target is real — otherwise the site ships dead links, +which cost more trust than a missing cross-reference. Until a target exists, link the +repository file it will be based on, or leave the link out. Check before committing: + +```bash +python3 - <<'EOF' +import pathlib, re +bad = [] +for md in pathlib.Path("user-docs").rglob("*.md"): + text = re.sub(r"```.*?```", "", md.read_text(), flags=re.S) + for m in re.finditer(r"\]\((?!https?:|#)([^)#]+)(#[^)]*)?\)", text): + if not (md.parent / m.group(1)).resolve().exists(): + bad.append(f"{md}: -> {m.group(1)}") +print("\n".join(bad) or "all relative links resolve") +EOF +``` + **Do not document what does not exist.** Verified against `docs/PROGRESS.md`. Firebase sign-in is built but off by default, MFA is gated, and report export, scheduled scans and multi-cloud are not started — none get a page. @@ -69,7 +87,25 @@ python3 -c "import xml.etree.ElementTree as ET,sys; [ET.parse(f) for f in sys.ar ### Writing a new page -Pick one of the three archetypes in the IA document — **task**, **explainer**, or -**reference** — and follow its shape. Depth increases down the page: the first screen serves -a non-technical reader, the last third serves someone debugging. That is what lets one site -serve both without splitting into audience tracks. +Copy the matching archetype and replace it. The guidance is inline, as comments that do not +render: + +| Archetype | Use for | Template | +| --- | --- | --- | +| **Task** | Someone doing a thing right now | [`task.md`](../design/docs-site/archetypes/task.md) | +| **Explainer** | Someone deciding whether to trust something | [`explainer.md`](../design/docs-site/archetypes/explainer.md) | +| **Reference** | Someone who arrived from search and wants to leave | [`reference.md`](../design/docs-site/archetypes/reference.md) | + +Seven rules, in full in the +[IA document](../design/docs-site/information-architecture.md#house-voice): + +1. **Depth increases down the page** — first screen for a non-technical reader, last third + for someone debugging. This is what lets one site serve three audiences. +2. **Write from the reader's side of the screen** — they manage accounts and findings, not + rows and rule evaluations. +3. **Symptom before cause** — troubleshooting is reached by searching an error message. +4. **Show real output**, not paraphrased output. +5. **Never claim more than the code does.** The child IAM role is not read-only, so no page + says it is. Every explainer carries a *What we do not claim* section. +6. **Link, do not restate.** Two canonical copies drift, and the stale one gets followed. +7. **No page for a feature that does not exist** — check `docs/PROGRESS.md`, not the roadmap. diff --git a/user-docs/start-here/how-tops-connects-to-aws.md b/user-docs/start-here/how-tops-connects-to-aws.md index 66b8256..5883f4c 100644 --- a/user-docs/start-here/how-tops-connects-to-aws.md +++ b/user-docs/start-here/how-tops-connects-to-aws.md @@ -159,9 +159,9 @@ publish the link — the scanner keeps working, the operational features stop. ## Related -- [What the IAM role can do](../aws-accounts/what-the-iam-role-can-do.md) — the same permissions, policy by policy -- [The security model](../security/the-security-model.md) — trust boundaries in full -- [Install](../start-here/install.md) — when you are ready to run it +- [Installing TOPS](https://github.com/teemops/tops#install) — the commands, kept in one place in the repository +- [The child-account CloudFormation template](https://github.com/teemops/tops/blob/develop/templates/iam.role.child.account.cfn.yaml) — every policy this page summarises, in full +- [Licence and trademark](https://github.com/teemops/tops/blob/develop/LICENSE) — Apache-2.0, with the name held separately *Source of truth for this page: `templates/iam.role.child.account.cfn.yaml`, `infra/cloud-stack/`, and `docker-compose.yml` in the TOPS repository.* From 864ed4cbe89056413a4ff6a5d633a9279464adc9 Mon Sep 17 00:00:00 2001 From: Ben Fellows Date: Sun, 2 Aug 2026 11:27:16 +1200 Subject: [PATCH 3/6] Add SECURITY.md, and stop README telling people to file vulnerabilities publicly A public repo with three open security issues and a reviewer-facing architecture page had nowhere to report a vulnerability. README's Support section said "open an issue", which for a vulnerability is the one thing you do not want someone to do, so that now carries a carve-out. Reporting is GitHub private advisories first, security@teemops.com as a fallback for people who will not use GitHub. Commitments are deliberately modest -- acknowledge in 5 working days, assess in 10 -- because a missed public promise costs more credibility than a slow one, and this is a small team. Two sections do more work than the boilerplate. "What we are most worried about" ranks the failure classes, led by cross-organisation data access, because telling researchers where to look is the point of having a policy. "Already known" lists #100, #101 and #102 with honest impact, so nobody spends a weekend rediscovering the open SNS topic and nobody reports it as though it were news. Scope draws the line self-hosted products have to draw: an operator with a shell on their own host is not a vulnerability, and neither is running with APP_DEBUG on a public address. BLOCKED ON TWO THINGS before this is true rather than aspirational: GitHub private vulnerability reporting is disabled on the repo and must be turned on, and security@teemops.com does not exist yet. Until both land the policy names channels that do not answer. Co-Authored-By: Claude Opus 5 --- README.md | 5 + SECURITY.md | 119 +++++++++++++++++++ design/docs-site/information-architecture.md | 10 +- 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 SECURITY.md diff --git a/README.md b/README.md index 80de679..c8d8f41 100644 --- a/README.md +++ b/README.md @@ -184,3 +184,8 @@ Two things sit alongside the licence: ## Support Open an issue. For commercial support, get in touch. + +**Found a security vulnerability?** Do not open an issue — +[report it privately](https://github.com/teemops/tops/security/advisories/new) or email +security@teemops.com. See [SECURITY.md](SECURITY.md) for scope, what to expect, and the +issues already known. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5161741 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,119 @@ +# Security policy + +TOPS is a security tool that reads customers' AWS accounts. A vulnerability here can expose +another organisation's cloud posture, so we would rather hear about a problem early and +awkwardly than late and politely. + +## Reporting a vulnerability + +**Do not open a public issue.** Use one of these instead: + +1. **[Report a vulnerability privately on GitHub](https://github.com/teemops/tops/security/advisories/new)** + — preferred. It keeps the report private, and lets us develop and review the fix in a + private fork before anything is disclosed. +2. **security@teemops.com** — if you would rather not use GitHub, or the form is unavailable. + +You do not need to have a fix, a CVE, or a polished write-up. A rough description of +something that looks wrong is worth sending. + +### What to include + +Whatever you have. The more of this you can give us, the faster we can confirm it: + +- What the issue is, and what an attacker gets out of it +- The steps to reproduce, ideally against a local install +- The version — `git rev-parse HEAD`, or the image tag from `docker compose images app` +- Whether you have told anyone else, and whether you intend to publish + +## What happens next + +| | | +| --- | --- | +| **Acknowledgement** | Within 5 working days | +| **Initial assessment** | Within 10 working days — whether we can reproduce it, and our severity view | +| **Fix and release** | Depends on severity. We will tell you the plan and keep you updated | +| **Credit** | Named in the advisory and the changelog, unless you would rather not be | + +TOPS is maintained by a small team. If you have not heard from us inside those windows, +please chase — the most likely explanation is that a message went astray, not that we are +ignoring it. + +## What we are most worried about + +If you are looking for somewhere to start, these are the failures that would hurt most, in +order: + +1. **Cross-organisation data access.** Every query touching tenant data is supposed to be + scoped by `organization_id`. Anything that lets one organisation read another's accounts, + scans or findings is the highest-severity class of bug in this codebase. +2. **Authentication or authorisation bypass** on any route. +3. **Anything that lets a third party influence account linking** — the CloudFormation + custom resource, the SNS to SQS path, or the handling of `external_id` — such that an + account is linked, unlinked, or repointed without the owner's action. +4. **Privilege escalation through the CloudFormation templates**, or a template that grants + more in a customer account than the documentation says it does. +5. **Secrets handling** — anything that writes credentials to logs, images, or the repository. + +## Scope + +**In scope** + +- The application in `app/` +- The CloudFormation templates in `templates/` and `infra/` +- The installer (`install.sh`), the Docker images, and `docker-compose.yml` +- The published `teem/tops` and `teem/tops-base` images + +**Out of scope** + +- **Anything that requires shell access to the host TOPS runs on.** This is a self-hosted + application; an operator with a shell can read the database by design. See + [D-9](docs/roadmap.md) on why application-level encryption at rest was deliberately removed. +- **Misconfiguration of your own install** — running with `APP_DEBUG=true` on a public + address, exposing MySQL's port, or reusing credentials. +- **Findings the scanner reports about your AWS account.** Those are the product working. If + you think a rule is wrong, open a normal issue. +- Missing security headers, cookie flags, or scanner output with no demonstrated impact. +- Denial of service against teemops.com, social engineering, and physical attacks. +- Vulnerabilities in AWS itself — report those to AWS. + +**Never test against infrastructure you do not own.** Reproduce against a local install or +your own AWS account. A report is not worth another organisation's data. + +## Already known — please do not report these + +These are public, tracked, and being worked on. Reporting them again is not a finding. + +| | | +| --- | --- | +| [#101](https://github.com/teemops/tops/issues/101) | The parent account's `teemops-sns` topic accepts `sns:Publish` from any AWS principal. Publish-only, and acting on a message still requires guessing a 122-bit `external_id`, but it is open and it is being fixed. The issue links the full research and the options considered | +| [#100](https://github.com/teemops/tops/issues/100) | The account-linking consumer does not cross-check the account in `TopsRoleArn` against the one in `StackId`, nor require a `pending` status | +| [#102](https://github.com/teemops/tops/issues/102) | `templates/iam.role.audit.account.cfn.yaml` defaults to a vendor AWS account and is not on the onboarding path | + +## Supported versions + +Only the latest release. TOPS is pre-1.0 and moving quickly; fixes land on `develop` and go +out in the next release rather than being backported. + +| Version | Supported | +| --- | --- | +| 0.4.x | Yes | +| < 0.4 | No — upgrade | + +Upgrading is a tag change and a restart. See +[Upgrading and rolling back](https://github.com/teemops/tops#upgrading-and-rolling-back). + +## Disclosure + +We aim to publish an advisory once a fix is released, describing the issue, the affected +versions and the fix. We will agree timing with you, and we would ask that you hold off +publishing until a fixed release exists — or 90 days from your report, whichever comes first. + +If a vulnerability is already being exploited, tell us and we will move immediately rather +than to any schedule. + +## What we will not do + +We will not pursue legal action against anyone who reports a vulnerability in good faith, +follows this policy, and does not access or destroy data that is not theirs. There is no +bug bounty — this is an open-source project without a budget for one, and we would rather +say so than imply otherwise. diff --git a/design/docs-site/information-architecture.md b/design/docs-site/information-architecture.md index 25b148e..8df91b1 100644 --- a/design/docs-site/information-architecture.md +++ b/design/docs-site/information-architecture.md @@ -164,9 +164,13 @@ job, which is the argument for doing the IA before the writing. ### Two gaps this inventory exposed -1. **There is no `SECURITY.md`.** A public repo with a `security` label, three open security - issues and a CISO-facing architecture page has nowhere to report a vulnerability. That is - a repo-hygiene fix, not a docs-site fix, and it should not wait for the site. +1. ~~**There is no `SECURITY.md`.**~~ **Written 2026-08-02.** A public repo with a `security` + label, three open security issues and a CISO-facing architecture page had nowhere to + report a vulnerability, and `README.md` said "open an issue" — precisely the wrong advice. + Now at the repository root, with the README pointing to it. **Two follow-ups it depends + on:** GitHub private vulnerability reporting must be enabled (it is currently off), and + `security@teemops.com` must be created and routed. Until both are done the policy names + channels that do not answer. 2. **`docs/quick-start.md` is dead.** It describes a Nuxt 3 + Serverless + Prisma monorepo — the pre-pivot stack. It is linked from `docs/README.md` and would mislead anyone who found it. Delete it rather than port it. From 78ab90445712111f13587c2e7a1295f300fa77f5 Mon Sep 17 00:00:00 2001 From: Ben Fellows Date: Sun, 2 Aug 2026 11:42:18 +1200 Subject: [PATCH 4/6] Document what the child IAM role can do, policy by policy The architecture page summarised the permissions honestly but at a level a reviewer will not accept on its own. This is the page they get pointed to next: 2 managed policies, 21 inline ones, which 3 are resource-scoped and which 18 carry Resource "*". Two things came out of reading the template properly rather than summarising it again. The write permissions are not used by anything. TOPS instantiates only STS, SNS and SQS clients; there is no SecurityHub, GuardDuty, Macie, Inspector, Config or CloudTrail code in the repository at all, and every one of the 36 operations declared across rules/tasks/*/tasks.json is list, get or describe. So the role asks for write access across eight services on behalf of features that do not exist. The page says exactly that, and shows how to strip them. And s3:ListAllMyBuckets in TopsS3Bucket can never authorise -- it does not support resource-level permissions and the statement scopes it to arn:aws:s3:::tops-config-*. Inert rather than dangerous, but a reviewer would find it, so we say it first. The architecture page said the write half "exists for the operational features that manage AWS security services on your behalf", which implied those features ship today. Corrected there too -- claiming more than the code does is the one thing this audience does not forgive. Explainer archetype amended: lead with whatever answers "what shape is this" fastest. Usually a diagram; on a permissions page an at-a-glance table beats one, because the shape is counts and categories. Co-Authored-By: Claude Opus 5 --- design/docs-site/archetypes/explainer.md | 8 +- design/docs-site/information-architecture.md | 2 +- user-docs/README.md | 3 +- .../aws-accounts/what-the-iam-role-can-do.md | 153 ++++++++++++++++++ .../start-here/how-tops-connects-to-aws.md | 8 +- 5 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 user-docs/aws-accounts/what-the-iam-role-can-do.md diff --git a/design/docs-site/archetypes/explainer.md b/design/docs-site/archetypes/explainer.md index 357e908..d656a4f 100644 --- a/design/docs-site/archetypes/explainer.md +++ b/design/docs-site/archetypes/explainer.md @@ -38,8 +38,12 @@ and what happens to it when you remove an account or uninstall. ![Description of what the diagram shows, written as a full sentence for screen readers and for anyone the image fails to load for.](../assets/diagrams/name.svg) -