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
175 changes: 175 additions & 0 deletions .scripts/run-opa-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/env bun
/**
* Script to run Regula OPA tests on all terraform policies.
*
* Usage:
* bun .scripts/run-opa-tests.ts [--filter <pattern>] [--verbose]
*
* Options:
* --filter <pat> Only run tests matching the pattern (e.g., "aws/AWS Backup")
* --verbose Show verbose output including test details
*
* Examples:
* bun .scripts/run-opa-tests.ts # Run all tests
* bun .scripts/run-opa-tests.ts --filter "AWS Lambda" # Run only Lambda tests
* bun .scripts/run-opa-tests.ts --filter gcp # Run only GCP tests
*/

import { spawnSync } from "child_process";
import { readdirSync, statSync } from "fs";
import { join, dirname } from "path";

const TERRAFORM_DIR = join(import.meta.dirname, "..", "terraform");

function findRegoFiles(dir: string): string[] {
const results: string[] = [];

function walk(currentDir: string) {
const entries = readdirSync(currentDir);
for (const entry of entries) {
const fullPath = join(currentDir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
walk(fullPath);
} else if (entry.endsWith(".rego")) {
results.push(fullPath);
}
}
}

walk(dir);
return results;
}

function findTestDirectories(baseDir: string): string[] {
const dirs = new Set<string>();
const regoFiles = findRegoFiles(baseDir);

for (const file of regoFiles) {
dirs.add(dirname(file));
}

return Array.from(dirs).sort();
}

interface TestResult {
path: string;
success: boolean;
output: string;
configsLoaded: number;
}

function runRegulaTest(dir: string): TestResult {
const result: TestResult = {
path: dir,
success: false,
output: "",
configsLoaded: 0
};

const proc = spawnSync("npx", ["regula-wasi", "test", dir], {
encoding: "utf-8",
timeout: 120000,
cwd: process.cwd()
});

result.output = (proc.stdout || "") + (proc.stderr || "");
result.success = proc.status === 0;
Comment on lines +70 to +77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle spawnSync errors/timeouts explicitly.
If npx is missing or a timeout occurs, proc.error/proc.status === null should be surfaced.

Proposed fix
 	const proc = spawnSync("npx", ["regula-wasi", "test", dir], {
 		encoding: "utf-8",
 		timeout: 120000,
 		cwd: process.cwd()
 	});
 
-	result.output = (proc.stdout || "") + (proc.stderr || "");
-	result.success = proc.status === 0;
+	if (proc.error) {
+		result.output = String(proc.error);
+		return result;
+	}
+	if (proc.status === null) {
+		result.output = (proc.stdout || "") + (proc.stderr || "") + "\nProcess terminated (timeout or signal).";
+		return result;
+	}
+	result.output = (proc.stdout || "") + (proc.stderr || "");
+	result.success = proc.status === 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const proc = spawnSync("npx", ["regula-wasi", "test", dir], {
encoding: "utf-8",
timeout: 120000,
cwd: process.cwd()
});
result.output = (proc.stdout || "") + (proc.stderr || "");
result.success = proc.status === 0;
const proc = spawnSync("npx", ["regula-wasi", "test", dir], {
encoding: "utf-8",
timeout: 120000,
cwd: process.cwd()
});
if (proc.error) {
result.output = String(proc.error);
return result;
}
if (proc.status === null) {
result.output = (proc.stdout || "") + (proc.stderr || "") + "\nProcess terminated (timeout or signal).";
return result;
}
result.output = (proc.stdout || "") + (proc.stderr || "");
result.success = proc.status === 0;
🤖 Prompt for AI Agents
In @.scripts/run-opa-tests.ts around lines 70 - 77, The current spawnSync call
in .scripts/run-opa-tests.ts assigns proc to the child result but doesn’t
surface spawn errors or timeouts; update the handling after spawnSync (the proc
variable) to explicitly check for proc.error and for proc.status === null
(timeout or killed) and set result.success = false, populate result.output with
proc.stdout/proc.stderr as before and add a result.error or result.failureReason
string that includes proc.error.message or a clear timeout/killed message
(include proc.signal if present) so failures from missing npx or timeouts are
surfaced for callers of this script.


// Extract number of loaded configs
const match = result.output.match(/Loaded (\d+) IaC configurations/);
if (match) {
result.configsLoaded = parseInt(match[1], 10);
}

return result;
}

async function main() {
const args = process.argv.slice(2);
const verbose = args.includes("--verbose");
const filterIndex = args.indexOf("--filter");
const filter = filterIndex !== -1 ? args[filterIndex + 1] : null;

console.log("=== Regula OPA Test Runner ===\n");

// Check regula-wasi version
const versionProc = spawnSync("npx", ["regula-wasi", "version"], {
encoding: "utf-8",
timeout: 30000
});

if (versionProc.status !== 0) {
console.error("Error: regula-wasi is not installed. Run: npm install --save-dev regula-wasi");
process.exit(1);
}

const versionOutput = (versionProc.stdout || "") + (versionProc.stderr || "");
const opaVersion = versionOutput.match(/OPA v([\d.]+)/)?.[1] || "unknown";
console.log(`Using regula-wasi with OPA v${opaVersion}\n`);

// Find all test directories
let testDirs = findTestDirectories(TERRAFORM_DIR);

if (filter) {
testDirs = testDirs.filter(d => d.toLowerCase().includes(filter.toLowerCase()));
console.log(`Filtering to ${testDirs.length} directories matching: ${filter}\n`);
Comment on lines +91 to +116
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against --filter without a value.
Right now it silently becomes falsy; better to fail fast.

Proposed fix
 	const verbose = args.includes("--verbose");
 	const filterIndex = args.indexOf("--filter");
-	const filter = filterIndex !== -1 ? args[filterIndex + 1] : null;
+	const filter = filterIndex !== -1 ? args[filterIndex + 1] : null;
+	if (filterIndex !== -1 && !filter) {
+		console.error("Error: --filter requires a value.");
+		process.exit(1);
+	}
🤖 Prompt for AI Agents
In @.scripts/run-opa-tests.ts around lines 91 - 116, The code currently allows
"--filter" with no following value which yields null/empty and silently
continues; update the parsing around args, filterIndex and filter so that after
computing filterIndex you verify args[filterIndex + 1] exists and is not another
flag (e.g., startsWith("-")), and if it's missing or invalid print a clear error
(e.g., console.error) and exit(1); apply this validation before using filter to
filter testDirs (the block that sets filter and filters testDirs), keeping
references to args, filterIndex, filter and leaving the rest of the flow
(findTestDirectories and TERRAFORM_DIR usage) unchanged.

}

console.log(`Found ${testDirs.length} test directories\n`);

if (testDirs.length === 0) {
console.log("No test directories found.");
return;
}

const results: TestResult[] = [];
let passed = 0;
let failed = 0;
let totalConfigs = 0;

for (const dir of testDirs) {
const relativePath = dir.replace(TERRAFORM_DIR + "/", "");
process.stdout.write(`Testing: ${relativePath}... `);

const result = runRegulaTest(dir);
totalConfigs += result.configsLoaded;

if (result.success) {
console.log(`✓ PASS (${result.configsLoaded} configs)`);
passed++;
} else {
console.log("✗ FAIL");
if (verbose) {
console.log(result.output);
}
failed++;
}

results.push(result);
}

console.log("\n=== Summary ===");
console.log(`Directories: ${testDirs.length}`);
console.log(`IaC Configs: ${totalConfigs}`);
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);

if (failed > 0) {
console.log("\nFailed tests:");
for (const result of results) {
if (!result.success) {
const relativePath = result.path.replace(TERRAFORM_DIR + "/", "");
console.log(` - ${relativePath}`);
}
}
process.exit(1);
}

console.log("\n✓ All tests passed!");
}

main().catch(e => {
console.error(e);
process.exit(1);
});
102 changes: 102 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Starchitect CloudGuard

Cloud security compliance policies and runtime checks for AWS and GCP.

## Project Structure

```
starchitect-cloudguard/
├── terraform/ # OPA/Rego security policies (IaC scanning)
│ ├── aws/ # AWS policies (66 service categories)
│ └── gcp/ # GCP policies (11 service categories)
├── runtime/ # TypeScript runtime checks (live cloud API)
│ ├── aws/ # AWS runtime checks with tests
│ └── gcp/ # GCP runtime checks with tests
├── cli/ # CLI application (oclif-based)
└── .scripts/ # Build and test scripts
```
Comment on lines +7 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language to the fenced block.
MD040 warning; use text (or similar).

Proposed fix
-```
+```text
 starchitect-cloudguard/
 ├── terraform/              # OPA/Rego security policies (IaC scanning)
 │   ├── aws/               # AWS policies (66 service categories)
 │   └── gcp/               # GCP policies (11 service categories)
 ├── runtime/               # TypeScript runtime checks (live cloud API)
 │   ├── aws/               # AWS runtime checks with tests
 │   └── gcp/               # GCP runtime checks with tests
 ├── cli/                   # CLI application (oclif-based)
 └── .scripts/              # Build and test scripts
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.20.0)</summary>

[warning] 7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @CLAUDE.md around lines 7 - 17, The fenced code block containing the project
tree in CLAUDE.md is missing a language tag (MD040); update the opening fence
from totext (or another suitable info string) so the block is recognized
as a text code fence; ensure only the opening fence is changed and the rest of
the block content (the tree lines) remains unchanged.


</details>

<!-- fingerprinting:phantom:poseidon:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->


## OPA/Rego Policies

Located in `terraform/aws/` and `terraform/gcp/`. Each policy directory contains:

- `*.rego` - Policy file using Fugue/Regula framework
- `*_pass.tf` - Terraform fixture that should pass the policy
- `*_fail.tf` - Terraform fixture that should fail the policy

### Running OPA Tests

```bash
# Run all OPA tests
npm run test:opa

# Run with verbose output
npm run test:opa:verbose

# Filter to specific policies
bun ./.scripts/run-opa-tests.ts --filter "AWS Lambda"
bun ./.scripts/run-opa-tests.ts --filter gcp
```

Uses `regula-wasi` (nonfx fork with OPA v1.12.2) to evaluate policies against test fixtures.

## Runtime Tests

TypeScript tests in `runtime/` use BUN test runner with Jest-style syntax:

```bash
# Run all runtime tests
npm test
# or
bun test
```

## Commands

| Command | Description |
| ------------------ | ---------------------------- |
| `npm test` | Run runtime tests (bun test) |
| `npm run test:opa` | Run OPA/Rego policy tests |
| `npm run build` | TypeScript compilation |
| `npm run lint` | Run ESLint |
| `npm run prettify` | Format code with Prettier |

## Writing Policies

Rego policies use the Fugue framework:

```rego
package rules.example_policy

import data.fugue

__rego__metadoc__ := {
"id": "EXAMPLE.1",
"title": "Example policy title",
"description": "Description of what this policy checks",
"custom": {"severity": "High", "author": "Starchitect Agent"},
}

resource_type := "MULTIPLE"

resources = fugue.resources("aws_example_resource")

policy[p] {
resource := resources[_]
# check passes
p = fugue.allow_resource(resource)
}

policy[p] {
resource := resources[_]
# check fails
p = fugue.deny_resource_with_message(resource, "Error message")
}
```

## Dependencies

- **regula-wasi**: OPA/Rego policy evaluation (WASI build with OPA v1.12.2)
- **bun**: Test runner and TypeScript execution
- **AWS SDK v3**: For runtime checks and test mocking
- **Google Cloud clients**: For GCP runtime checks
18 changes: 16 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"scripts": {
"build": "tsc -b --incremental",
"test": "bun test",
"test:opa": "bun ./.scripts/run-opa-tests.ts",
"test:opa:verbose": "bun ./.scripts/run-opa-tests.ts --verbose",
"prepare": "husky",
"prettier:lint": "prettier --config .prettierrc.cjs --cache --cache-location .cache/prettier --check .",
"lint:files": "env TIMING=1 eslint --quiet",
Expand Down Expand Up @@ -72,8 +74,8 @@
"@google-cloud/monitoring": "^4.1.0",
"@google-cloud/resource-manager": "^5.3.0",
"@google-cloud/service-usage": "^3.4.0",
"@google-cloud/storage": "^7.15.0",
"@google-cloud/sql": "^0.19.0",
"@google-cloud/storage": "^7.15.0",
"@googleapis/sqladmin": "^24.0.0",
"@types/bun": "^1.1.14",
"@types/jest": "^29.5.14",
Expand All @@ -85,6 +87,7 @@
"jest": "^29.7.0",
"lint-staged": "^15.2.11",
"prettier": "^3.4.2",
"regula-wasi": "^3.2.3",
"ts-jest": "^29.2.5",
"typescript-eslint": "^8.18.2"
},
Expand Down