Skip to content
Draft
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,39 @@ Domains automatically match all subdomains:
sudo awf --allow-domains github.com "curl https://api.github.com" # ✓ works
```

### Using a Domains File

For longer domain lists, use a file (one domain per line):

```bash
# Create a domains file
cat > allowed-domains.txt << EOF
# GitHub domains
github.com
api.github.com
githubusercontent.com

# Other services
googleapis.com
arxiv.org
EOF

# Use the file
sudo awf --allow-domains-file allowed-domains.txt "curl https://api.github.com"

# Combine both methods
sudo awf \
--allow-domains example.com \
--allow-domains-file allowed-domains.txt \
"curl https://api.github.com"
```

**File Format:**
- One domain per line
- Lines starting with `#` are treated as comments
- Empty lines are ignored
- Whitespace is trimmed from each line

Common domain lists:

```bash
Expand Down
97 changes: 96 additions & 1 deletion src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Command } from 'commander';
import { parseEnvironmentVariables } from './cli';
import { parseEnvironmentVariables, parseDomainsFromFile } from './cli';
import { redactSecrets } from './redact-secrets';
import { parseDomains } from './cli';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

describe('cli', () => {
describe('domain parsing', () => {
Expand Down Expand Up @@ -36,6 +39,98 @@ describe('cli', () => {
});
});

describe('domain parsing from file', () => {
let tempDir: string;

beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-test-'));
});

afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});

it('should parse domains from file with one domain per line', () => {
const filePath = path.join(tempDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\napi.github.com\nnpmjs.org');

const result = parseDomainsFromFile(filePath);

expect(result).toEqual(['github.com', 'api.github.com', 'npmjs.org']);
});

it('should filter out empty lines', () => {
const filePath = path.join(tempDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\n\napi.github.com\n\n\nnpmjs.org\n');

const result = parseDomainsFromFile(filePath);

expect(result).toEqual(['github.com', 'api.github.com', 'npmjs.org']);
});

it('should filter out comment lines starting with #', () => {
const filePath = path.join(tempDir, 'domains.txt');
fs.writeFileSync(filePath, '# This is a comment\ngithub.com\n# Another comment\napi.github.com');

const result = parseDomainsFromFile(filePath);

expect(result).toEqual(['github.com', 'api.github.com']);
});

it('should trim whitespace from each line', () => {
const filePath = path.join(tempDir, 'domains.txt');
fs.writeFileSync(filePath, ' github.com \n\tapi.github.com\t\n npmjs.org ');

const result = parseDomainsFromFile(filePath);

expect(result).toEqual(['github.com', 'api.github.com', 'npmjs.org']);
});

it('should handle file with mixed content', () => {
const filePath = path.join(tempDir, 'domains.txt');
fs.writeFileSync(filePath,
'# GitHub domains\ngithub.com\n api.github.com \n\n# NPM registry\nnpmjs.org\n# End of file\n'
);

const result = parseDomainsFromFile(filePath);

expect(result).toEqual(['github.com', 'api.github.com', 'npmjs.org']);
});

it('should handle empty file', () => {
const filePath = path.join(tempDir, 'domains.txt');
fs.writeFileSync(filePath, '');

const result = parseDomainsFromFile(filePath);

expect(result).toEqual([]);
});

it('should handle file with only comments and whitespace', () => {
const filePath = path.join(tempDir, 'domains.txt');
fs.writeFileSync(filePath, '# Comment 1\n\n \n# Comment 2\n\t\n');

const result = parseDomainsFromFile(filePath);

expect(result).toEqual([]);
});

it('should throw error for non-existent file', () => {
const filePath = path.join(tempDir, 'nonexistent.txt');

expect(() => parseDomainsFromFile(filePath)).toThrow();
});

it('should handle file with Windows line endings', () => {
const filePath = path.join(tempDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\r\napi.github.com\r\nnpmjs.org\r\n');

const result = parseDomainsFromFile(filePath);

expect(result).toEqual(['github.com', 'api.github.com', 'npmjs.org']);
});
});

describe('environment variable parsing', () => {
it('should parse KEY=VALUE format correctly', () => {
const envVars = ['GITHUB_TOKEN=abc123', 'API_KEY=xyz789'];
Expand Down
46 changes: 43 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Command } from 'commander';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { WrapperConfig, LogLevel } from './types';
import { logger } from './logger';
import {
Expand Down Expand Up @@ -32,6 +33,20 @@ export function parseDomains(input: string): string[] {
.filter(d => d.length > 0);
}

/**
* Parses domains from a file, one domain per line
* @param filePath - Path to the file containing domains
* @returns Array of trimmed domain strings with empty lines and comments filtered out
* @throws Error if file cannot be read
*/
export function parseDomainsFromFile(filePath: string): string[] {
const content = fs.readFileSync(filePath, 'utf-8');
return content
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && !line.startsWith('#'));
}

/**
* Result of parsing environment variables
*/
Expand Down Expand Up @@ -71,10 +86,14 @@ program
.name('awf')
.description('Network firewall for agentic workflows with domain whitelisting')
.version('0.1.0')
.requiredOption(
.option(
'--allow-domains <domains>',
'Comma-separated list of allowed domains (e.g., github.com,api.github.com)'
)
.option(
'--allow-domains-file <path>',
'Path to file containing allowed domains (one per line)'
)
.option(
'--log-level <level>',
'Log level: debug, info, warn, error',
Expand Down Expand Up @@ -127,10 +146,31 @@ program

logger.setLevel(logLevel);

const allowedDomains = parseDomains(options.allowDomains);
// Parse domains from both sources
let allowedDomains: string[] = [];

// Parse from --allow-domains flag if provided
if (options.allowDomains) {
allowedDomains = parseDomains(options.allowDomains);
}

// Parse from --allow-domains-file if provided
if (options.allowDomainsFile) {
try {
const domainsFromFile = parseDomainsFromFile(options.allowDomainsFile);
allowedDomains = [...allowedDomains, ...domainsFromFile];
} catch (error) {
logger.error(`Failed to read domains file: ${options.allowDomainsFile}`);
logger.error(String(error));
process.exit(1);
}
}

// Remove duplicates
allowedDomains = [...new Set(allowedDomains)];

if (allowedDomains.length === 0) {
logger.error('At least one domain must be specified with --allow-domains');
logger.error('At least one domain must be specified with --allow-domains or --allow-domains-file');
process.exit(1);
}

Expand Down