Skip to content
Merged
Changes from 2 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
123 changes: 123 additions & 0 deletions tests/integration/credential-hiding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,129 @@
}, 120000);
});

describe('All 14 Credential Paths Coverage', () => {
// These tests cover the 11 credential paths not tested by Tests 1-4 above.
// Each path is hidden via /dev/null mount and should return empty content.

const untestedPaths = [
{ name: 'SSH id_rsa', path: '.ssh/id_rsa' },
{ name: 'SSH id_ed25519', path: '.ssh/id_ed25519' },
{ name: 'SSH id_ecdsa', path: '.ssh/id_ecdsa' },
{ name: 'SSH id_dsa', path: '.ssh/id_dsa' },
{ name: 'AWS credentials', path: '.aws/credentials' },
{ name: 'AWS config', path: '.aws/config' },
{ name: 'Kube config', path: '.kube/config' },
{ name: 'Azure credentials', path: '.azure/credentials' },
{ name: 'GCloud credentials.db', path: '.config/gcloud/credentials.db' },
{ name: 'Cargo credentials', path: '.cargo/credentials' },
{ name: 'Composer auth.json', path: '.composer/auth.json' },
];

// Track files we create so we only clean up what we added
const createdFiles: string[] = [];
const createdDirs: string[] = [];

beforeAll(() => {
// Create dummy credential files on the host so AWF will mount /dev/null over them.
// Without these files existing, AWF skips the /dev/null mount and the files
// simply don't exist inside the container.
const homeDir = os.homedir();
for (const p of untestedPaths) {
const fullPath = `${homeDir}/${p.path}`;
if (!fs.existsSync(fullPath)) {
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
createdDirs.push(dir);
}
fs.writeFileSync(fullPath, 'DUMMY_SECRET_VALUE');

Check failure

Code scanning / CodeQL

Potential file system race condition High test

The file may have changed since it
was checked
.
createdFiles.push(fullPath);
}
}
});

afterAll(() => {
// Clean up only the files/dirs we created
for (const f of createdFiles) {
try { fs.unlinkSync(f); } catch { /* ignore */ }
}
// Remove dirs in reverse order (deepest first)
for (const d of createdDirs.reverse()) {
try { fs.rmdirSync(d); } catch { /* ignore if not empty */ }
}
});

test('All untested credential files are hidden at direct home path (0 bytes)', async () => {
const homeDir = os.homedir();
const paths = untestedPaths.map(p => `${homeDir}/${p.path}`).join(' ');
Comment on lines +289 to +290
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

Building a shell command by interpolating paths directly into sh -c '...' is unsafe and can break when homeDir contains spaces or shell-special characters (and is also an injection risk since this runs with sudo). Prefer passing file paths as positional args to sh -c and iterating over "$@" (or otherwise robustly quoting/escaping each path) so the loop receives the exact paths regardless of characters.

Suggested change
const homeDir = os.homedir();
const paths = untestedPaths.map(p => `${homeDir}/${p.path}`).join(' ');
const paths = untestedPaths.map(p => `"$HOME/${p.path}"`).join(' ');

Copilot uses AI. Check for mistakes.

// Check all credential files in a single container run for efficiency.
// wc -c reports byte count; /dev/null-mounted files should be 0 bytes.
// Use '|| true' to prevent failures when files don't exist
const result = await runner.runWithSudo(
`sh -c 'for f in ${paths}; do if [ -f "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`,
{
allowDomains: ['github.com'],
logLevel: 'debug',
timeout: 60000,
}
);

expect(result).toSucceed();
const cleanOutput = extractCommandOutput(result.stdout);
const lines = cleanOutput.split('\n').filter(l => l.match(/^\s*\d+/));
// Each file should be 0 bytes (hidden via /dev/null)
lines.forEach(line => {
const size = parseInt(line.trim().split(/\s+/)[0]);
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

parseInt should be called with an explicit radix to avoid edge-case parsing issues. Use parseInt(value, 10) here (and in the similar parsing block in the /host test) to make the intent unambiguous.

Copilot uses AI. Check for mistakes.
expect(size).toBe(0);
});
// Verify we checked all 11 files
expect(lines.length).toBe(untestedPaths.length);
}, 120000);

test('All untested credential files are hidden at /host path (0 bytes)', async () => {
const homeDir = os.homedir();
const paths = untestedPaths.map(p => `/host${homeDir}/${p.path}`).join(' ');

const result = await runner.runWithSudo(
`sh -c 'for f in ${paths}; do if [ -f "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`,
{
allowDomains: ['github.com'],
logLevel: 'debug',
timeout: 60000,
}
);

expect(result).toSucceed();
const cleanOutput = extractCommandOutput(result.stdout);
const lines = cleanOutput.split('\n').filter(l => l.match(/^\s*\d+/));
lines.forEach(line => {
const size = parseInt(line.trim().split(/\s+/)[0]);
expect(size).toBe(0);
});
expect(lines.length).toBe(untestedPaths.length);
}, 120000);

test('cat on each untested credential file returns empty content', async () => {
const homeDir = os.homedir();
const paths = untestedPaths.map(p => `${homeDir}/${p.path}`).join(' ');

// cat all files and concatenate output - should be empty
const result = await runner.runWithSudo(
`sh -c 'for f in ${paths}; do if [ -f "$f" ]; then cat "$f"; fi; done 2>&1 || true'`,
{
allowDomains: ['github.com'],
logLevel: 'debug',
timeout: 60000,
}
);

expect(result).toSucceed();
// All content should be empty (no credential data leaked)
const cleanOutput = extractCommandOutput(result.stdout).trim();
expect(cleanOutput).toBe('');
}, 120000);
});

describe('Security Verification', () => {
test('Test 12: Simulated exfiltration attack gets empty data', async () => {
Expand Down
Loading