Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Detailed documentation is available in the `docs/` directory:
- [Database Documentation](docs/database/README.md) - Database structure and diagrams
- [Cron Jobs](docs/cron-jobs.md) - Scheduled tasks
- [Analytics](docs/analytics.md) - Event tracking and analytics
- [AWS Support Evidence Package](docs/security/aws-support-evidence-package.md) - Redacted incident evidence bundle for S3/IAM remediation

## Development

Expand Down Expand Up @@ -204,4 +205,3 @@ END $$
DELIMITER;



44 changes: 44 additions & 0 deletions docs/security/aws-support-evidence-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# AWS Support Evidence Package

Issue #55 includes a final evidence package for requesting AWS restriction review after the StudentHub S3/IAM remediation is complete. Keep raw screenshots, CloudTrail exports, IAM CSV files, Railway screens, candidate records, and full key IDs in a private incident folder. This helper only produces a redacted Markdown summary that can be reviewed before sending to AWS Support.

## Usage

Prepare a private JSON manifest using the sample shape in `tools/fixtures/aws-support-evidence-package.sample.json`, then run:

```bash
node tools/build-aws-support-evidence-package.mjs private/aws-support-evidence.json > private/aws-support-evidence.md
```

The generated report redacts:

- 12-digit AWS account IDs
- AWS access-key-looking values
- long secret-like token strings

It preserves short key suffixes such as `FZMN`, `4T67K`, `ODY2X`, and `WCUM` because issue #55 uses suffixes as the public-safe reference format.

## Manifest Sections

The manifest should contain only evidence summaries and local/private file references:

- `deletedKeys`: inactive key suffixes deleted after screenshots were captured
- `rotatedKeys`: exposed or over-permissioned key suffixes rotated or replaced
- `environmentVariables`: replacement variable names configured in Railway/secret stores
- `bucketControls`: S3 lifecycle, CORS, versioning, logging, replication, and public-access evidence
- `smokeTests`: post-remediation upload, remove, replace, staff-view, and key-deactivation tests
- `cloudTrail`: investigation summaries with source IP, user agent, event, bucket, and key suffix only
- `iamReviews`: least-privilege/access-analyzer review summaries
- `supportNotes`: remaining limitations, private attachment locations, or follow-up owner notes

Do not put full keys, secret keys, bearer tokens, raw CloudTrail JSON, candidate Civil ID values/images, phone numbers, account IDs, or payment/tax data into the manifest.

## Validation

Run the local regression check before committing changes to this helper:

```bash
node tools/check-aws-support-evidence-package.mjs
node --check tools/build-aws-support-evidence-package.mjs
node --check tools/check-aws-support-evidence-package.mjs
```
110 changes: 110 additions & 0 deletions tools/build-aws-support-evidence-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env node

import fs from "node:fs";

const [, , manifestPath] = process.argv;

if (!manifestPath) {
console.error(
"Usage: node tools/build-aws-support-evidence-package.mjs <manifest.json>",
);
process.exit(1);
}

const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));

function redact(value) {
if (value == null) {
return "";
}

return String(value)
.replace(/\b\d{12}\b/g, "[aws-account-id-redacted]")
.replace(/\bA(KIA|SIA|GPA|IDA)[A-Z0-9]{16}\b/g, "[aws-access-key-redacted]")
.replace(
/\b(?=[A-Za-z0-9+/=_-]{32,}\b)(?=.*[A-Z])(?=.*[a-z])(?=.*\d)[A-Za-z0-9+/=_-]{32,}\b/g,
"[secret-like-value-redacted]",
);
}

function line(value) {
return redact(value || "not provided");
}

function list(values) {
if (!Array.isArray(values) || values.length === 0) {
return "- None provided\n";
}

return values.map((value) => `- ${line(value)}`).join("\n") + "\n";
}

function table(headers, rows) {
const safeRows = Array.isArray(rows) ? rows : [];
const header = `| ${headers.join(" | ")} |`;
const divider = `| ${headers.map(() => "---").join(" | ")} |`;
const body = safeRows.map(
(row) => `| ${headers.map((key) => line(row[key])).join(" | ")} |`,
);

return [header, divider, ...body].join("\n") + "\n";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function envTable(rows) {
const safeRows = Array.isArray(rows) ? rows : [];
return table(
["service", "names", "evidence"],
safeRows.map((row) => ({
service: row.service,
names: Array.isArray(row.names) ? row.names.join(", ") : row.names,
evidence: row.evidence,
})),
);
}

const incident = manifest.incident || {};

const sections = [
`# ${line(incident.title || "AWS Support Evidence Package")}`,
"",
`Prepared by: ${line(incident.preparedBy)}`,
`Prepared at: ${line(incident.preparedAt)}`,
`Private evidence folder: ${line(incident.privateEvidenceFolder)}`,
"",
"## Deleted Inactive Keys",
table(["suffix", "iamUser", "status", "deletedAt", "evidence"], manifest.deletedKeys),
"## Rotated Or Deactivated Keys",
table(["suffix", "iamUser", "replacement", "status", "evidence"], manifest.rotatedKeys),
"## Replacement Environment Variables",
envTable(manifest.environmentVariables),
"## Bucket Controls",
table(["bucket", "control", "status", "evidence"], manifest.bucketControls),
"## Smoke Tests",
table(["name", "status", "evidence"], manifest.smokeTests),
"## CloudTrail Review Summary",
table(
[
"eventTime",
"eventName",
"userName",
"accessKeySuffix",
"sourceIPAddress",
"userAgent",
"bucketName",
"result",
],
manifest.cloudTrail,
),
"## IAM Review Summary",
table(["iamUser", "status", "evidence"], manifest.iamReviews),
"## Support Notes",
list(manifest.supportNotes),
"## Public Safety Checklist",
"- Full access keys are redacted or represented by suffix only.",
"- Secret-like values are redacted.",
"- AWS account IDs are redacted.",
"- Candidate Civil ID images, values, phone numbers, raw exports, and payment/tax data are not included.",
"",
];

process.stdout.write(sections.join("\n"));
57 changes: 57 additions & 0 deletions tools/check-aws-support-evidence-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const fixturePath = "tools/fixtures/aws-support-evidence-package.sample.json";
const manifest = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
manifest.supportNotes = [
...(manifest.supportNotes || []),
`Synthetic redaction probe: key ${"AKIA"}${"ABCDEFGHIJKLMNOP"} must not render.`,
];

const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "aws-support-evidence-"));
const tmpManifest = path.join(tmpDir, "manifest.json");
fs.writeFileSync(tmpManifest, JSON.stringify(manifest, null, 2));

const output = execFileSync(process.execPath, [
"tools/build-aws-support-evidence-package.mjs",
tmpManifest,
], { encoding: "utf8" });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const required = [
"Deleted Inactive Keys",
"Rotated Or Deactivated Keys",
"Replacement Environment Variables",
"Bucket Controls",
"Smoke Tests",
"CloudTrail Review Summary",
"IAM Review Summary",
"Public Safety Checklist",
"FZMN",
"4T67K",
"ODY2X",
"WCUM",
];

for (const text of required) {
if (!output.includes(text)) {
throw new Error(`Expected generated report to include: ${text}`);
}
}

const forbidden = [
/\b\d{12}\b/,
/\bA(KIA|SIA|GPA|IDA)[A-Z0-9]{16}\b/,
/Abcdefghijklmnopqrstuvwxyz123456/,
];

for (const pattern of forbidden) {
if (pattern.test(output)) {
throw new Error(`Generated report leaked forbidden pattern: ${pattern}`);
}
}

console.log("AWS Support evidence package check passed.");
103 changes: 103 additions & 0 deletions tools/fixtures/aws-support-evidence-package.sample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"incident": {
"title": "StudentHub S3/IAM remediation evidence",
"preparedBy": "Security team",
"preparedAt": "2026-05-15T10:30:00Z",
"privateEvidenceFolder": "private/aws-support-2026-05"
},
"deletedKeys": [
{
"suffix": "FZMN",
"iamUser": "textract-access",
"status": "deleted",
"deletedAt": "2026-05-15T09:00:00Z",
"evidence": "screenshots/fzmn-deleted.png"
},
{
"suffix": "4T67K",
"iamUser": "public-environment-s3-access",
"status": "deleted",
"deletedAt": "2026-05-15T09:05:00Z",
"evidence": "screenshots/4t67k-deleted.png"
}
],
"rotatedKeys": [
{
"suffix": "ODY2X",
"iamUser": "public-environment-s3-access",
"replacement": "AWS_TEMP_BUCKET_KEY / AWS_TEMP_BUCKET_SECRET",
"status": "rotated and deactivated",
"evidence": "screenshots/ody2x-rotated.png"
},
{
"suffix": "WCUM",
"iamUser": "railway-s3-access",
"replacement": "AWS_PERMANENT_S3_ACCESS_KEY_ID / AWS_PERMANENT_S3_SECRET_ACCESS_KEY",
"status": "rotated and deactivated",
"evidence": "screenshots/wcum-rotated.png"
}
],
"environmentVariables": [
{
"service": "Railway production",
"names": [
"AWS_TEMP_BUCKET_KEY",
"AWS_TEMP_BUCKET_SECRET",
"AWS_PERMANENT_S3_ACCESS_KEY_ID",
"AWS_PERMANENT_S3_SECRET_ACCESS_KEY"
],
"evidence": "screenshots/railway-env-names-only.png"
}
],
"bucketControls": [
{
"bucket": "studenthub-uploads",
"control": "lifecycle rule PressureDelete3Days disabled",
"status": "complete",
"evidence": "screenshots/studenthub-uploads-lifecycle-disabled.png"
},
{
"bucket": "studenthub-uploads",
"control": "versioning enabled",
"status": "complete",
"evidence": "screenshots/studenthub-uploads-versioning.png"
}
],
"smokeTests": [
{
"name": "new candidate signup with Civil ID front/back",
"status": "passed",
"evidence": "smoke-tests/new-candidate-civil-id.md"
},
{
"name": "existing candidate Civil ID remove",
"status": "passed",
"evidence": "smoke-tests/remove-civil-id.md"
}
],
"cloudTrail": [
{
"eventTime": "2026-04-18T11:22:33Z",
"eventName": "PutBucketLifecycleConfiguration",
"userName": "railway-s3-access",
"accessKeySuffix": "WCUM",
"sourceIPAddress": "203.0.113.10",
"userAgent": "aws-sdk-php/3.x",
"bucketName": "studenthub-uploads",
"result": "reviewed with expected automation owner"
}
],
"iamReviews": [
{
"iamUser": "railway-s3-access",
"status": "restricted to object-level S3 actions",
"evidence": "screenshots/railway-s3-policy-after.png"
}
],
"supportNotes": [
"Raw screenshots and exports remain in the private evidence folder.",
"Synthetic redaction probe: account 123456789012 must not render.",
"Synthetic redaction probe: token Abcdefghijklmnopqrstuvwxyz123456 must not render.",
"No full access keys or candidate Civil ID data are included in this public manifest."
]
}