Skip to content
Merged
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
38 changes: 26 additions & 12 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ ignite run <path> [options]
| `--skip-preflight` | `false` | Skip safety checks |
| `--json` | `false` | Output results as JSON |
| `--audit` | `false` | Run with security audit (blocks network, read-only filesystem) |
| `--audit-output <file>` | - | Write security audit to a JSON file |

**Examples:**

Expand Down Expand Up @@ -151,6 +152,8 @@ Filesystem
✗ Security Status: 2 VIOLATION(S) BLOCKED
```

When `--json` is used with `--audit`, the JSON output includes a `securityAudit` field.

**Output:**

```
Expand Down Expand Up @@ -574,14 +577,17 @@ Execute a service.
"data": [1, 2, 3],
"operation": "sum"
},
"skipPreflight": false
"skipPreflight": false,
"audit": true
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `input` | object | No | Input data passed to service |
| `skipPreflight` | boolean | No | Skip safety checks |
| `skipBuild` | boolean | No | Skip image build if already built |
| `audit` | boolean | No | Run with security audit |

**Response:**

Expand All @@ -597,6 +603,8 @@ Execute a service.
}
```

When `audit` is true, the response includes `securityAudit`.

**Errors:**

| Status | Description |
Expand Down Expand Up @@ -636,6 +644,23 @@ service:
timeoutMs: number # Timeout (default: 30000)
env: object # Environment variables
dependencies: array # Explicit dependencies (auto-detected by default)

preflight:
memory:
baseMb: number # Base memory estimate (default: 50)
perDependencyMb: number # Memory per dependency (default: 2)
warnRatio: number # Warning threshold ratio (default: 1)
failRatio: number # Failure threshold ratio (default: 0.8)
dependencies:
warnCount: number # Warn if dependency count exceeds (default: 100)
infoCount: number # Info threshold for moderate count (default: 50)
image:
warnMb: number # Image size warn threshold (default: 500)
failMb: number # Image size fail threshold (default: 1000)
timeout:
minMs: number # Minimum timeout (default: 100)
maxMs: number # Maximum recommended timeout (default: 30000)
coldStartBufferMs: number # Cold start buffer (default: 500)
```

**Supported Runtimes:**
Expand Down Expand Up @@ -699,20 +724,9 @@ Create an `ignite.policy.yaml` file to customize security settings:
security:
network:
enabled: false # Block all network (default)
allowedHosts: # Optional: allow specific hosts
- api.example.com
allowedPorts: # Optional: allow specific ports
- 443

filesystem:
readOnly: true # Read-only root filesystem
allowedWritePaths: # Paths that can be written to
- /tmp
blockedReadPaths: # Paths blocked from reading
- /etc/passwd
- /etc/shadow
- /proc
- /sys

process:
allowSpawn: false # Block spawning child processes
Expand Down
22 changes: 18 additions & 4 deletions docs/preflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,26 @@ Each check returns:

## Customizing Thresholds

Thresholds are currently hardcoded. Future versions will support configuration via `service.yaml`:
Thresholds can be configured via `service.yaml`:

```yaml
service:
name: my-service
preflight:
memory_buffer: 1.5
max_dependencies: 150

preflight:
memory:
baseMb: 60
perDependencyMb: 3
warnRatio: 1
failRatio: 0.85
dependencies:
warnCount: 120
infoCount: 60
image:
warnMb: 600
failMb: 1200
timeout:
minMs: 200
maxMs: 45000
coldStartBufferMs: 750
```
2 changes: 1 addition & 1 deletion examples/hello-bun/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ service:
runtime: bun
entry: index.ts
memoryMb: 128
timeoutMs: 5000
timeoutMs: 30000
env:
NODE_ENV: production
26 changes: 21 additions & 5 deletions packages/cli/src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { loadService, executeService, runPreflight, createReport, formatReportAsText, getImageName, buildServiceImage, parseAuditFromOutput, formatSecurityAudit, DEFAULT_POLICY, isValidRuntime } from '@ignite/core';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { loadService, executeService, runPreflight, createReport, formatReportAsText, getImageName, buildServiceImage, parseAuditFromOutput, formatSecurityAudit, DEFAULT_POLICY, isValidRuntime, loadPolicyFile } from '@ignite/core';
import { logger, ConfigError } from '@ignite/shared';

interface RunOptions {
Expand All @@ -7,6 +9,7 @@ interface RunOptions {
json?: boolean;
audit?: boolean;
runtime?: string;
auditOutput?: string;
}

export async function runCommand(servicePath: string, options: RunOptions): Promise<void> {
Expand Down Expand Up @@ -45,18 +48,31 @@ export async function runCommand(servicePath: string, options: RunOptions): Prom
process.exit(1);
}

const metrics = await executeService(service, { input, skipBuild: true, audit: options.audit });
const policy = options.audit
? (await loadPolicyFile(service.servicePath)) ?? DEFAULT_POLICY
: undefined;

const metrics = await executeService(service, { input, skipBuild: true, audit: options.audit, policy });

const report = createReport(preflightResult, metrics);
const audit = options.audit && policy
? parseAuditFromOutput(metrics.stdout, metrics.stderr, policy)
: undefined;

if (options.auditOutput && audit) {
const outputPath = join(process.cwd(), options.auditOutput);
await writeFile(outputPath, JSON.stringify(audit, null, 2));
logger.success(`Audit saved to ${outputPath}`);
}
Comment on lines +62 to +66
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

Potential issue with absolute paths in auditOutput.

Using join(process.cwd(), options.auditOutput) will produce incorrect results if auditOutput is already an absolute path (e.g., /tmp/audit.json becomes /current/dir/tmp/audit.json).

Suggested fix using resolve()
-      const outputPath = join(process.cwd(), options.auditOutput);
+      const outputPath = resolve(options.auditOutput);

resolve() correctly handles both relative and absolute paths - relative paths are resolved against cwd(), while absolute paths are returned as-is. Note: resolve is already imported from node:path via line 2.

📝 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
if (options.auditOutput && audit) {
const outputPath = join(process.cwd(), options.auditOutput);
await writeFile(outputPath, JSON.stringify(audit, null, 2));
logger.success(`Audit saved to ${outputPath}`);
}
if (options.auditOutput && audit) {
const outputPath = resolve(options.auditOutput);
await writeFile(outputPath, JSON.stringify(audit, null, 2));
logger.success(`Audit saved to ${outputPath}`);
}
🤖 Prompt for AI Agents
In `@packages/cli/src/commands/run.ts` around lines 62 - 66, Replace the path
construction that uses join(process.cwd(), options.auditOutput) so absolute
auditOutput paths are preserved; update the code that writes the audit (the
block using options.auditOutput, writeFile and logger.success) to compute the
output path with resolve(process.cwd(), options.auditOutput) (or
resolve(options.auditOutput)) from node:path instead of join, then pass that
resolved path to writeFile and logger.success.


if (options.json) {
console.log(JSON.stringify(report, null, 2));
const payload = audit ? { ...report, securityAudit: audit } : report;
console.log(JSON.stringify(payload, null, 2));
} else {
console.log(formatReportAsText(report));
}

if (options.audit) {
const audit = parseAuditFromOutput(metrics.stdout, metrics.stderr, DEFAULT_POLICY);
if (options.audit && audit && !options.json) {
console.log(formatSecurityAudit(audit));
}

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ program
.option('--skip-preflight', 'Skip preflight checks before execution')
.option('--json', 'Output results as JSON')
.option('--audit', 'Run with security audit (blocks network, read-only filesystem)')
.option('--audit-output <file>', 'Write security audit to a JSON file')
.action(runCommand);

program
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/__tests__/load-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('loadService', () => {
expect(service.config.service.runtime).toBe('bun');
expect(service.config.service.entry).toBe('index.ts');
expect(service.config.service.memoryMb).toBe(128);
expect(service.config.service.timeoutMs).toBe(5000);
expect(service.config.service.timeoutMs).toBe(30000);
expect(service.servicePath).toBe(servicePath);
});

Expand Down
18 changes: 11 additions & 7 deletions packages/core/src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { LoadedService } from '../service/service.types.js';
import { dockerBuild, dockerRun, isDockerAvailable } from '../runtime/docker-runtime.js';
import { getRuntimeConfig } from '../runtime/runtime-registry.js';
import { parseMetrics } from './metrics.js';
import { DEFAULT_POLICY, loadPolicyFile, policyToDockerOptions } from '../security/index.js';
import type { SecurityPolicy } from '../security/security.types.js';



Expand All @@ -14,6 +16,7 @@ export interface ExecuteOptions {
env?: Record<string, string>;
skipBuild?: boolean;
audit?: boolean;
policy?: SecurityPolicy;
}

interface ExecutionState {
Expand Down Expand Up @@ -45,6 +48,13 @@ export async function executeService(

logger.info(`Executing ${serviceName}...`);

let policy: SecurityPolicy | undefined;
if (options.audit) {
policy = options.policy ?? (await loadPolicyFile(service.servicePath)) ?? DEFAULT_POLICY;
}

const securityOptions = policy ? policyToDockerOptions(policy) : undefined;

const runResult = await dockerRun({
imageName,
containerName,
Expand All @@ -65,13 +75,7 @@ export async function executeService(
IGNITE_INPUT: options.input ? JSON.stringify(options.input) : '',
NODE_ENV: 'production',
},
security: options.audit ? {
networkDisabled: true,
readOnlyRootfs: true,
dropCapabilities: true,
noNewPrivileges: true,
tmpfsPaths: ['/tmp'],
} : undefined,
security: options.audit ? securityOptions : undefined,
});

const metrics = parseMetrics(runResult, isColdStart);
Expand Down
30 changes: 20 additions & 10 deletions packages/core/src/preflight/analyze-image.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import type { PreflightCheck } from '@ignite/shared';
import { getImageInfo } from '../runtime/docker-runtime.js';

const IMAGE_SIZE_WARN_THRESHOLD_MB = 500;
const IMAGE_SIZE_FAIL_THRESHOLD_MB = 1000;
const DEFAULT_IMAGE_SIZE_WARN_MB = 500;
const DEFAULT_IMAGE_SIZE_FAIL_MB = 1000;

export async function analyzeImage(imageName: string): Promise<PreflightCheck> {
export interface ImagePreflightConfig {
warnMb?: number;
failMb?: number;
}

export async function analyzeImage(
imageName: string,
config?: ImagePreflightConfig
): Promise<PreflightCheck> {
const imageInfo = await getImageInfo(imageName);
const warnThresholdMb = config?.warnMb ?? DEFAULT_IMAGE_SIZE_WARN_MB;
const failThresholdMb = config?.failMb ?? DEFAULT_IMAGE_SIZE_FAIL_MB;

if (!imageInfo) {
return {
Expand All @@ -17,23 +27,23 @@ export async function analyzeImage(imageName: string): Promise<PreflightCheck> {

const sizeMb = Math.round(imageInfo.size / 1024 / 1024);

if (sizeMb > IMAGE_SIZE_FAIL_THRESHOLD_MB) {
if (sizeMb > failThresholdMb) {
return {
name: 'image-size',
status: 'fail',
message: `Image size ${sizeMb}MB exceeds ${IMAGE_SIZE_FAIL_THRESHOLD_MB}MB limit`,
message: `Image size ${sizeMb}MB exceeds ${failThresholdMb}MB limit`,
value: sizeMb,
threshold: IMAGE_SIZE_FAIL_THRESHOLD_MB,
threshold: failThresholdMb,
};
}

if (sizeMb > IMAGE_SIZE_WARN_THRESHOLD_MB) {
if (sizeMb > warnThresholdMb) {
return {
name: 'image-size',
status: 'warn',
message: `Image size ${sizeMb}MB exceeds recommended ${IMAGE_SIZE_WARN_THRESHOLD_MB}MB`,
message: `Image size ${sizeMb}MB exceeds recommended ${warnThresholdMb}MB`,
value: sizeMb,
threshold: IMAGE_SIZE_WARN_THRESHOLD_MB,
threshold: warnThresholdMb,
};
}

Expand All @@ -42,6 +52,6 @@ export async function analyzeImage(imageName: string): Promise<PreflightCheck> {
status: 'pass',
message: `Image size ${sizeMb}MB is within limits`,
value: sizeMb,
threshold: IMAGE_SIZE_WARN_THRESHOLD_MB,
threshold: warnThresholdMb,
};
}
Loading