Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ __pycache__/
# Emitted by sdk/typescript's build:ci script.
/.github/scripts/check_plugin_source_compatibility.mjs
/.github/scripts/test_check_plugin_source_compatibility.mjs

# Emitted by sdk/typescript's build:examples script.
/examples/custom-validation/app.mjs
/examples/custom-validation/validate.mjs
19 changes: 14 additions & 5 deletions examples/custom-validation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

This deliberately vulnerable invoice API contains only synthetic data. Do not
deploy it. The validation script starts a real loopback HTTP server, tests
cross-account access, saves the evidence, and stops the server. It needs Python
3.10 or later and no extra packages or Docker.
cross-account access, saves the evidence, and stops the server. It uses the
SDK's supported Node.js version (including 22.13) and TypeScript compiler, with no
extra packages or Docker.

From the repository root, build the CLI and run the demo:

Expand All @@ -14,8 +15,16 @@ node examples/custom-validation/run.mjs
```

The runner uses your existing Codex Security sign-in or API key. It copies the
fixture to a temporary directory and prints the scan output path. Extra CLI
options can be appended, for example `--model gpt-5.6-terra --effort high`.
TypeScript fixture to a temporary directory, compiles it to JavaScript, and
prints the scan output path. Extra CLI options can be appended, for example
`--model gpt-5.6-terra --effort high`.

To run just the HTTP proof without a scan:

```bash
pnpm --dir sdk/typescript run build:examples
node examples/custom-validation/validate.mjs --output reports/http-proof.json
```

Look for these files in the printed scan directory:

Expand All @@ -31,4 +40,4 @@ cannot complete; it does not fall back to the default validation workflow.

Adapt [validation.md](validation.md) for your own setup, tests, and cleanup.
For a Docker-based project, the same prompt can run your existing compose or
test script instead of `validate.py`.
test script instead of `validate.mjs`.
58 changes: 58 additions & 0 deletions examples/custom-validation/app.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Deliberately vulnerable local fixture. Do not deploy this application.
import { once } from "node:events";
import { createServer as createHttpServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import { pathToFileURL } from "node:url";

// These identities, tokens, and records are synthetic demo data.
const tokens = new Map([
["demo-alice", "alice"],
["demo-bob", "bob"],
]);
type Invoice = { id: string; owner: string; amount: number };
const invoices = new Map<string, Invoice>([
["1001", { id: "1001", owner: "alice", amount: 25 }],
["1002", { id: "1002", owner: "bob", amount: 80 }],
]);

export async function createServer(): Promise<Server> {
const server = createHttpServer((request, response) => {
function reply(status: number, body: Invoice | { error: string }): void {
const encoded = JSON.stringify(body);
response.writeHead(status, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(encoded),
});
response.end(encoded);
}

if (request.method !== "GET") {
response.writeHead(501).end();
return;
}
const authorization = request.headers.authorization ?? "";
const token = authorization.startsWith("Bearer ")
? authorization.slice("Bearer ".length)
: authorization;
const user = tokens.get(token);
if (user === undefined) return reply(401, { error: "unauthorized" });
const path = request.url ?? "";
if (!path.startsWith("/invoices/"))
return reply(404, { error: "not found" });
const invoice = invoices.get(path.slice("/invoices/".length));
if (invoice === undefined) return reply(404, { error: "not found" });
// BUG: authentication does not establish ownership of this invoice.
reply(200, invoice);
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
return server;
}

if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
const server = await createServer();
console.log(`http://127.0.0.1:${(server.address() as AddressInfo).port}`);
}
48 changes: 0 additions & 48 deletions examples/custom-validation/app.py

This file was deleted.

25 changes: 23 additions & 2 deletions examples/custom-validation/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,31 @@ const root = await mkdtemp(join(tmpdir(), "codex-security-validation-demo-"));
const target = join(root, "target");
const output = join(root, "scan");
await mkdir(target);
for (const name of ["app.py", "validate.py"]) {
for (const name of ["app.mts", "validate.mts"]) {
await copyFile(new URL(name, import.meta.url), join(target, name));
}

const build = spawnSync(
process.execPath,
[
fileURLToPath(
new URL(
"../../sdk/typescript/node_modules/typescript/bin/tsc",
import.meta.url,
),
),
"--project",
fileURLToPath(
new URL("../../sdk/typescript/tsconfig.examples.json", import.meta.url),
),
"--outDir",
target,
],
{ stdio: "inherit" },
);
if (build.error) throw build.error;
if (build.status !== 0) process.exit(build.status ?? 1);

console.log(`Demo target: ${target}\nScan output: ${output}`);
const child = spawnSync(
process.execPath,
Expand All @@ -22,7 +43,7 @@ const child = spawnSync(
"scan",
target,
"--path",
"app.py",
"app.mts",
"--scan-prompt-file",
fileURLToPath(new URL("scan.md", import.meta.url)),
"--validation-prompt-file",
Expand Down
4 changes: 2 additions & 2 deletions examples/custom-validation/scan.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Review invoice ownership checks in `app.py`. An authenticated account must not
Review invoice ownership checks in `app.mts`. An authenticated account must not
read another account's invoice. The fixed tokens and records are synthetic test
data, not production credentials. `validate.py` is a test harness, not an
data, not production credentials. `validate.mts` is a test harness, not an
application endpoint. Keep discovery source-only.
79 changes: 79 additions & 0 deletions examples/custom-validation/validate.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Exercise the fixture over real HTTP and save the observed evidence.
import assert from "node:assert/strict";
import { mkdir, writeFile } from "node:fs/promises";
import type { AddressInfo } from "node:net";
import { dirname } from "node:path";
import { parseArgs } from "node:util";
import { createServer } from "./app.mjs";

type HttpResult = {
status: number;
body: { id?: string; owner?: string; amount?: number; error?: string };
};

async function main(): Promise<number> {
let output: string;
try {
const { values } = parseArgs({
options: {
output: { type: "string" },
help: { type: "boolean", short: "h" },
},
});
if (values.help) {
console.log("Usage: node validate.mjs --output PATH");
return 0;
}
if (values.output === undefined) throw new Error("--output is required");
output = values.output;
} catch (error) {
console.error((error as Error).message);
return 2;
}

const server = await createServer();
const baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
async function get(invoice: string, token?: string): Promise<HttpResult> {
const response = await fetch(`${baseUrl}/invoices/${invoice}`, {
headers: token === undefined ? {} : { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(5_000),
});
return {
status: response.status,
body: (await response.json()) as HttpResult["body"],
};
}

let evidence: {
anonymous: HttpResult;
own_invoice: HttpResult;
other_invoice: HttpResult;
cross_account_read: boolean;
};
try {
const anonymous = await get("1002");
const own_invoice = await get("1001", "demo-alice");
const other_invoice = await get("1002", "demo-alice");
assert.equal(anonymous.status, 401, "Authentication control failed");
assert.equal(own_invoice.status, 200, "Own-account control failed");
evidence = {
anonymous,
own_invoice,
other_invoice,
cross_account_read:
other_invoice.status === 200 && other_invoice.body.owner === "bob",
};
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}

const proof = { ...evidence, server_stopped: true };
await mkdir(dirname(output), { recursive: true });
await writeFile(output, `${JSON.stringify(proof, null, 2)}\n`, "utf8");
console.log(JSON.stringify(proof));
return 0;
}

process.exitCode = await main();
60 changes: 0 additions & 60 deletions examples/custom-validation/validate.py

This file was deleted.

4 changes: 2 additions & 2 deletions examples/custom-validation/validation.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Validate the invoice-ownership finding against this local fixture.

1. Use the configured Python interpreter to run `validate.py` from the supplied
1. Run `node validate.mjs` from the supplied
repository root. Pass `--output` with the absolute path to
`artifacts/custom-validation/http-proof.json` inside this scan's directory.
Set `PYTHONDONTWRITEBYTECODE=1` so the target remains unchanged.
The runner has already compiled the TypeScript fixture to JavaScript.
2. The script starts a server on an ephemeral `127.0.0.1` port, makes three HTTP
requests using synthetic identities, and shuts the server down. This local
server is the only authorized test target. Do not install packages or contact
Expand Down
3 changes: 2 additions & 1 deletion sdk/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
"build": "node --run clean && tsc -p tsconfig.build.json && node scripts/build-dashboard.mjs",
"build:ci": "tsc -p tsconfig.ci.json",
"build:examples": "tsc -p tsconfig.examples.json",
"build:plugin": "node scripts/build-plugin.mjs",
"check:plugin-source": "node scripts/check-plugin-source.mjs",
"check:package": "node scripts/check-package.mjs",
"format": "prettier --check --ignore-path .gitignore --ignore-path .prettierignore \"**/*.{cjs,mjs,js,ts,mts,tsx,json,md}\" \"../../.github/scripts/*.mts\"",
"format": "prettier --check --ignore-path .gitignore --ignore-path .prettierignore \"**/*.{cjs,mjs,js,ts,mts,tsx,json,md}\" \"../../.github/scripts/*.mts\" \"../../examples/custom-validation/*.{mts,md}\"",
"generate:models": "node scripts/generate-models.cjs",
"generate:models:check": "node scripts/generate-models.cjs --check",
"lint": "tsc --noEmit",
Expand Down
Loading
Loading