middlebrick 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # middlebrick
2
+
3
+ CLI for [middleBrick](https://middlebrick.com) API security scanning. Scan APIs from your terminal, get risk scores, and gate CI/CD pipelines.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g middlebrick
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Set up your API key
15
+ middlebrick configure
16
+
17
+ # Scan an API
18
+ middlebrick scan https://api.example.com/v1/users
19
+
20
+ # JSON output for CI
21
+ middlebrick scan https://api.example.com/v1/users --format json
22
+
23
+ # Fail if score < 80
24
+ middlebrick scan https://api.example.com/v1/users --threshold 80
25
+ ```
26
+
27
+ ## Commands
28
+
29
+ ### `middlebrick scan <url>`
30
+
31
+ Scan an API endpoint and display security results.
32
+
33
+ | Flag | Description | Default |
34
+ |------|-------------|---------|
35
+ | `--method <METHOD>` | HTTP method | `GET` |
36
+ | `--format <FORMAT>` | Output: `summary`, `json`, `table` | `summary` |
37
+ | `--threshold <score>` | Exit code 1 if score below threshold | — |
38
+ | `--no-wait` | Return scanId immediately | — |
39
+
40
+ ### `middlebrick results <scanId>`
41
+
42
+ Fetch results of a previous scan.
43
+
44
+ ### `middlebrick configure`
45
+
46
+ Interactively set up your API key. Saves to `~/.middlebrick/config.json`.
47
+
48
+ ## Authentication
49
+
50
+ | Priority | Source |
51
+ |----------|--------|
52
+ | 1 | `--api-key` flag |
53
+ | 2 | `MIDDLEBRICK_API_KEY` env var |
54
+ | 3 | `~/.middlebrick/config.json` |
55
+
56
+ ## Exit Codes
57
+
58
+ | Code | Meaning |
59
+ |------|---------|
60
+ | 0 | Success |
61
+ | 1 | Scan failed or score below threshold |
62
+ | 2 | Auth/config error |
63
+
64
+ ## CI/CD
65
+
66
+ ```yaml
67
+ # GitHub Actions
68
+ - name: API Security Gate
69
+ run: npx middlebrick scan ${{ env.API_URL }} --threshold 75 --api-key ${{ secrets.MIDDLEBRICK_API_KEY }}
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT — [Zevlat Intelligence](https://zev.lat)
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const configureCommand: Command;
@@ -0,0 +1,33 @@
1
+ import { Command } from "commander";
2
+ import { createInterface } from "node:readline";
3
+ import { saveConfig, loadConfig } from "../config.js";
4
+ export const configureCommand = new Command("configure")
5
+ .description("Set up your middleBrick API key")
6
+ .action(async () => {
7
+ const existing = loadConfig();
8
+ const rl = createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ });
12
+ const prompt = existing.apiKey
13
+ ? `API key [${existing.apiKey.slice(0, 6)}...]: `
14
+ : "API key: ";
15
+ const apiKey = await new Promise((resolve) => {
16
+ rl.question(prompt, (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim());
19
+ });
20
+ });
21
+ if (!apiKey) {
22
+ if (existing.apiKey) {
23
+ console.log("Keeping existing key.");
24
+ }
25
+ else {
26
+ console.error("No key provided. Get one at https://middlebrick.com/dashboard");
27
+ process.exit(2);
28
+ }
29
+ return;
30
+ }
31
+ saveConfig({ apiKey });
32
+ console.log("API key saved to ~/.middlebrick/config.json");
33
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const resultsCommand: Command;
@@ -0,0 +1,47 @@
1
+ import { Command } from "commander";
2
+ import { MiddleBrickClient, MiddleBrickError } from "@middlebrick/api-client";
3
+ import { resolveApiKey } from "../config.js";
4
+ import { formatSummary } from "../formatters/summary.js";
5
+ import { formatJson } from "../formatters/json.js";
6
+ import { formatTable } from "../formatters/table.js";
7
+ export const resultsCommand = new Command("results")
8
+ .description("Fetch results of a previous scan")
9
+ .argument("<scanId>", "Scan ID to retrieve")
10
+ .option("--format <FORMAT>", "Output format: summary | json | table", "summary")
11
+ .action(async (scanId, opts, cmd) => {
12
+ const globalOpts = cmd.optsWithGlobals();
13
+ let apiKey;
14
+ try {
15
+ apiKey = resolveApiKey(globalOpts);
16
+ }
17
+ catch (err) {
18
+ console.error(err.message);
19
+ process.exit(2);
20
+ }
21
+ const client = new MiddleBrickClient({
22
+ apiKey,
23
+ baseUrl: globalOpts.baseUrl,
24
+ });
25
+ try {
26
+ const result = await client.getScan(scanId);
27
+ switch (opts.format) {
28
+ case "json":
29
+ console.log(formatJson(result));
30
+ break;
31
+ case "table":
32
+ console.log(formatTable(result));
33
+ break;
34
+ case "summary":
35
+ default:
36
+ console.log(formatSummary(result));
37
+ break;
38
+ }
39
+ }
40
+ catch (err) {
41
+ if (err instanceof MiddleBrickError) {
42
+ console.error(`Error: ${err.message}`);
43
+ process.exit(err.statusCode === 401 || err.statusCode === 403 ? 2 : 1);
44
+ }
45
+ throw err;
46
+ }
47
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const scanCommand: Command;
@@ -0,0 +1,103 @@
1
+ import { Command } from "commander";
2
+ import { MiddleBrickClient, MiddleBrickError } from "@middlebrick/api-client";
3
+ import { resolveApiKey } from "../config.js";
4
+ import { formatSummary } from "../formatters/summary.js";
5
+ import { formatJson } from "../formatters/json.js";
6
+ import { formatTable } from "../formatters/table.js";
7
+ export const scanCommand = new Command("scan")
8
+ .description("Scan an API endpoint for security risks")
9
+ .argument("<url>", "API endpoint URL to scan")
10
+ .option("--method <METHOD>", "HTTP method", "GET")
11
+ .option("--format <FORMAT>", "Output format: summary | json | table", "summary")
12
+ .option("--threshold <score>", "Exit code 1 if overall score < threshold", parseFloat)
13
+ .option("--no-wait", "Return scanId immediately, don't poll")
14
+ .action(async (url, opts, cmd) => {
15
+ const globalOpts = cmd.optsWithGlobals();
16
+ let apiKey;
17
+ try {
18
+ apiKey = resolveApiKey(globalOpts);
19
+ }
20
+ catch (err) {
21
+ console.error(err.message);
22
+ process.exit(2);
23
+ }
24
+ const client = new MiddleBrickClient({
25
+ apiKey,
26
+ baseUrl: globalOpts.baseUrl,
27
+ });
28
+ let result;
29
+ try {
30
+ if (opts.wait === false) {
31
+ if (opts.threshold !== undefined) {
32
+ console.error("Warning: --threshold is ignored with --no-wait");
33
+ }
34
+ const { scanId } = await client.scanApi({
35
+ apiUrl: url,
36
+ method: opts.method,
37
+ });
38
+ console.log(scanId);
39
+ return;
40
+ }
41
+ // Show spinner while waiting
42
+ const spinner = createSpinner("Scanning...");
43
+ spinner.start();
44
+ try {
45
+ result = await client.scanAndWait({
46
+ apiUrl: url,
47
+ method: opts.method,
48
+ });
49
+ }
50
+ finally {
51
+ spinner.stop();
52
+ }
53
+ }
54
+ catch (err) {
55
+ if (err instanceof MiddleBrickError) {
56
+ console.error(`Error: ${err.message}`);
57
+ process.exit(err.statusCode === 401 || err.statusCode === 403 ? 2 : 1);
58
+ }
59
+ throw err;
60
+ }
61
+ // Format output
62
+ const output = formatOutput(result, opts.format, opts.threshold);
63
+ console.log(output);
64
+ // Threshold check
65
+ if (opts.threshold !== undefined &&
66
+ result.overall !== undefined &&
67
+ result.overall < opts.threshold) {
68
+ process.exit(1);
69
+ }
70
+ if (result.status === "failed") {
71
+ process.exit(1);
72
+ }
73
+ });
74
+ function formatOutput(result, format, threshold) {
75
+ switch (format) {
76
+ case "json":
77
+ return formatJson(result);
78
+ case "table":
79
+ return formatTable(result);
80
+ case "summary":
81
+ default:
82
+ return formatSummary(result, threshold);
83
+ }
84
+ }
85
+ function createSpinner(message) {
86
+ let interval = null;
87
+ let dots = 0;
88
+ return {
89
+ start() {
90
+ process.stdout.write(message);
91
+ interval = setInterval(() => {
92
+ dots = (dots + 1) % 4;
93
+ process.stdout.write(`\r${message}${".".repeat(dots)}${" ".slice(dots)}`);
94
+ }, 300);
95
+ },
96
+ stop() {
97
+ if (interval) {
98
+ clearInterval(interval);
99
+ process.stdout.write("\r" + " ".repeat(message.length + 4) + "\r");
100
+ }
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,11 @@
1
+ interface Config {
2
+ apiKey?: string;
3
+ }
4
+ declare const CONFIG_DIR: string;
5
+ declare const CONFIG_FILE: string;
6
+ export declare function resolveApiKey(opts: {
7
+ apiKey?: string;
8
+ }): string;
9
+ export declare function loadConfig(): Config;
10
+ export declare function saveConfig(config: Config): void;
11
+ export { CONFIG_DIR, CONFIG_FILE };
package/dist/config.js ADDED
@@ -0,0 +1,41 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const CONFIG_DIR = join(homedir(), ".middlebrick");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
+ export function resolveApiKey(opts) {
7
+ // 1. CLI flag
8
+ if (opts.apiKey)
9
+ return opts.apiKey;
10
+ // 2. Environment variable
11
+ const envKey = process.env.MIDDLEBRICK_API_KEY;
12
+ if (envKey)
13
+ return envKey;
14
+ // 3. Config file
15
+ const config = loadConfig();
16
+ if (config.apiKey)
17
+ return config.apiKey;
18
+ throw new Error([
19
+ "No API key found. Set one with:",
20
+ "",
21
+ " middlebrick configure",
22
+ " --api-key <key>",
23
+ " MIDDLEBRICK_API_KEY=mb_... middlebrick scan <url>",
24
+ "",
25
+ "Get your API key at https://middlebrick.com/dashboard",
26
+ ].join("\n"));
27
+ }
28
+ export function loadConfig() {
29
+ try {
30
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
31
+ return JSON.parse(raw);
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ export function saveConfig(config) {
38
+ mkdirSync(CONFIG_DIR, { recursive: true });
39
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
40
+ }
41
+ export { CONFIG_DIR, CONFIG_FILE };
@@ -0,0 +1,2 @@
1
+ import type { ScanResult } from "@middlebrick/api-client";
2
+ export declare function formatJson(result: ScanResult): string;
@@ -0,0 +1,3 @@
1
+ export function formatJson(result) {
2
+ return JSON.stringify(result, null, 2);
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScanResult } from "@middlebrick/api-client";
2
+ export declare function formatSummary(result: ScanResult, threshold?: number): string;
@@ -0,0 +1,97 @@
1
+ import chalk from "chalk";
2
+ function scoreBar(score) {
3
+ const filled = Math.round(score / 10);
4
+ const empty = 10 - filled;
5
+ return chalk.green("█".repeat(filled)) + chalk.gray("░".repeat(empty));
6
+ }
7
+ function gradeColor(grade) {
8
+ switch (grade) {
9
+ case "A":
10
+ return chalk.green(grade);
11
+ case "B":
12
+ return chalk.cyan(grade);
13
+ case "C":
14
+ return chalk.yellow(grade);
15
+ case "D":
16
+ return chalk.hex("#FF8800")(grade);
17
+ case "F":
18
+ return chalk.red(grade);
19
+ default:
20
+ return grade;
21
+ }
22
+ }
23
+ function severityIcon(severity) {
24
+ switch (severity) {
25
+ case "critical":
26
+ case "high":
27
+ case "medium":
28
+ return chalk.red("✗");
29
+ case "low":
30
+ return chalk.yellow("!");
31
+ case "info":
32
+ return chalk.green("✓");
33
+ default:
34
+ return " ";
35
+ }
36
+ }
37
+ function severityLabel(severity) {
38
+ const upper = severity.toUpperCase();
39
+ switch (severity) {
40
+ case "critical":
41
+ return chalk.bgRed.white(` ${upper} `);
42
+ case "high":
43
+ return chalk.red(`[${upper}]`);
44
+ case "medium":
45
+ return chalk.yellow(`[${upper}]`);
46
+ case "low":
47
+ return chalk.cyan(`[${upper}]`);
48
+ case "info":
49
+ return chalk.gray(`[${upper}]`);
50
+ default:
51
+ return `[${upper}]`;
52
+ }
53
+ }
54
+ export function formatSummary(result, threshold) {
55
+ const lines = [];
56
+ lines.push("");
57
+ lines.push(chalk.bold(" middleBrick API Security Scan"));
58
+ lines.push("");
59
+ lines.push(` URL: ${result.apiUrl}`);
60
+ lines.push(` Method: ${result.method}`);
61
+ if (result.status === "failed") {
62
+ lines.push("");
63
+ lines.push(chalk.red(` Scan failed: ${result.errorMessage || "Unknown error"}`));
64
+ lines.push("");
65
+ return lines.join("\n");
66
+ }
67
+ if (result.overall !== undefined && result.letterGrade) {
68
+ lines.push(` Score: ${result.overall}/100 (${gradeColor(result.letterGrade)})`);
69
+ }
70
+ if (result.categories && Object.keys(result.categories).length > 0) {
71
+ lines.push("");
72
+ lines.push(chalk.bold(" Categories:"));
73
+ for (const [name, score] of Object.entries(result.categories)) {
74
+ const label = name.padEnd(20);
75
+ lines.push(` ${label} ${scoreBar(score)} ${score}`);
76
+ }
77
+ }
78
+ const findings = result.findings || [];
79
+ if (findings.length > 0) {
80
+ lines.push("");
81
+ lines.push(chalk.bold(` Findings (${findings.length}):`));
82
+ for (const f of findings) {
83
+ lines.push(` ${severityIcon(f.severity)} ${severityLabel(f.severity)} ${f.title}`);
84
+ lines.push(` ${chalk.gray(f.description)}`);
85
+ }
86
+ }
87
+ if (threshold !== undefined && result.overall !== undefined) {
88
+ lines.push("");
89
+ const passed = result.overall >= threshold;
90
+ const status = passed
91
+ ? chalk.green("PASS ✓")
92
+ : chalk.red("FAIL ✗");
93
+ lines.push(` Threshold: ${threshold} → ${status}`);
94
+ }
95
+ lines.push("");
96
+ return lines.join("\n");
97
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScanResult } from "@middlebrick/api-client";
2
+ export declare function formatTable(result: ScanResult): string;
@@ -0,0 +1,9 @@
1
+ export function formatTable(result) {
2
+ const findings = result.findings || [];
3
+ if (findings.length === 0) {
4
+ return "No findings.";
5
+ }
6
+ const header = "SEVERITY\tCATEGORY\tTITLE";
7
+ const rows = findings.map((f) => `${f.severity.toUpperCase()}\t${f.category}\t${f.title}`);
8
+ return [header, ...rows].join("\n");
9
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, join } from "node:path";
6
+ import { scanCommand } from "./commands/scan.js";
7
+ import { resultsCommand } from "./commands/results.js";
8
+ import { configureCommand } from "./commands/configure.js";
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
11
+ const program = new Command()
12
+ .name("middlebrick")
13
+ .description("middleBrick — API security scanning from your terminal")
14
+ .version(pkg.version)
15
+ .option("--api-key <key>", "API key (overrides env/config)")
16
+ .option("--base-url <url>", "API base URL override");
17
+ program.addCommand(scanCommand);
18
+ program.addCommand(resultsCommand);
19
+ program.addCommand(configureCommand);
20
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "middlebrick",
3
+ "version": "0.1.0",
4
+ "description": "middleBrick CLI — API security scanning from your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "middlebrick": "./dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "test": "vitest run"
14
+ },
15
+ "dependencies": {
16
+ "@middlebrick/api-client": "^0.1.0",
17
+ "chalk": "^5.4.1",
18
+ "commander": "^13.1.0"
19
+ },
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=18"
23
+ }
24
+ }