mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
feat(policy): implement project policy integrity verification
Adds a security mechanism to detect and prompt for confirmation when project-level policies are added or modified. This prevents unauthorized policy changes from being applied silently. - PolicyIntegrityManager calculates and persists policy directory hashes. - Config integrates integrity checks during startup. - PolicyUpdateDialog prompts users in interactive mode. - --accept-changed-policies flag supports non-interactive workflows. - toml-loader refactored to expose file reading logic.
This commit is contained in:
@@ -374,6 +374,13 @@ export interface McpEnablementCallbacks {
|
||||
isFileEnabled: (serverId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface PolicyUpdateConfirmationRequest {
|
||||
scope: string;
|
||||
identifier: string;
|
||||
policyDir: string;
|
||||
newHash: string;
|
||||
}
|
||||
|
||||
export interface ConfigParameters {
|
||||
sessionId: string;
|
||||
clientVersion?: string;
|
||||
@@ -454,6 +461,7 @@ export interface ConfigParameters {
|
||||
eventEmitter?: EventEmitter;
|
||||
useWriteTodos?: boolean;
|
||||
policyEngineConfig?: PolicyEngineConfig;
|
||||
policyUpdateConfirmationRequest?: PolicyUpdateConfirmationRequest;
|
||||
output?: OutputSettings;
|
||||
disableModelRouterForAuth?: AuthType[];
|
||||
continueOnFailedApiCall?: boolean;
|
||||
@@ -631,6 +639,9 @@ export class Config {
|
||||
private readonly useWriteTodos: boolean;
|
||||
private readonly messageBus: MessageBus;
|
||||
private readonly policyEngine: PolicyEngine;
|
||||
private readonly policyUpdateConfirmationRequest:
|
||||
| PolicyUpdateConfirmationRequest
|
||||
| undefined;
|
||||
private readonly outputSettings: OutputSettings;
|
||||
private readonly continueOnFailedApiCall: boolean;
|
||||
private readonly retryFetchErrors: boolean;
|
||||
@@ -846,6 +857,8 @@ export class Config {
|
||||
approvalMode:
|
||||
params.approvalMode ?? params.policyEngineConfig?.approvalMode,
|
||||
});
|
||||
this.policyUpdateConfirmationRequest =
|
||||
params.policyUpdateConfirmationRequest;
|
||||
this.messageBus = new MessageBus(this.policyEngine, this.debugMode);
|
||||
this.acknowledgedAgentsService = new AcknowledgedAgentsService();
|
||||
this.skillManager = new SkillManager();
|
||||
@@ -1714,6 +1727,12 @@ export class Config {
|
||||
return this.policyEngine.getApprovalMode();
|
||||
}
|
||||
|
||||
getPolicyUpdateConfirmationRequest():
|
||||
| PolicyUpdateConfirmationRequest
|
||||
| undefined {
|
||||
return this.policyUpdateConfirmationRequest;
|
||||
}
|
||||
|
||||
setApprovalMode(mode: ApprovalMode): void {
|
||||
if (!this.isTrustedFolder() && mode !== ApprovalMode.DEFAULT) {
|
||||
throw new Error(
|
||||
|
||||
@@ -93,6 +93,10 @@ export class Storage {
|
||||
);
|
||||
}
|
||||
|
||||
static getPolicyIntegrityStoragePath(): string {
|
||||
return path.join(Storage.getGlobalGeminiDir(), 'policy_integrity.json');
|
||||
}
|
||||
|
||||
private static getSystemConfigDir(): string {
|
||||
if (os.platform() === 'darwin') {
|
||||
return '/Library/Application Support/GeminiCli';
|
||||
|
||||
@@ -17,6 +17,7 @@ export * from './policy/types.js';
|
||||
export * from './policy/policy-engine.js';
|
||||
export * from './policy/toml-loader.js';
|
||||
export * from './policy/config.js';
|
||||
export * from './policy/integrity.js';
|
||||
export * from './confirmation-bus/types.js';
|
||||
export * from './confirmation-bus/message-bus.js';
|
||||
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { PolicyIntegrityManager, IntegrityStatus } from './integrity.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../config/storage.js', () => ({
|
||||
Storage: {
|
||||
getPolicyIntegrityStoragePath: vi
|
||||
.fn()
|
||||
.mockReturnValue('/mock/storage/policy_integrity.json'),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./toml-loader.js', () => ({
|
||||
readPolicyFiles: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock FS
|
||||
const mockFs = vi.hoisted(() => ({
|
||||
readFile: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
default: mockFs,
|
||||
readFile: mockFs.readFile,
|
||||
writeFile: mockFs.writeFile,
|
||||
mkdir: mockFs.mkdir,
|
||||
}));
|
||||
|
||||
describe('PolicyIntegrityManager', () => {
|
||||
let integrityManager: PolicyIntegrityManager;
|
||||
let readPolicyFilesMock: Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
const { readPolicyFiles } = await import('./toml-loader.js');
|
||||
readPolicyFilesMock = readPolicyFiles as Mock;
|
||||
integrityManager = new PolicyIntegrityManager();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('checkIntegrity', () => {
|
||||
it('should return NEW if no stored hash', async () => {
|
||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); // No stored file
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
||||
]);
|
||||
|
||||
const result = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/dir',
|
||||
);
|
||||
expect(result.status).toBe(IntegrityStatus.NEW);
|
||||
expect(result.hash).toBeDefined();
|
||||
expect(result.hash).toHaveLength(64);
|
||||
expect(result.fileCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should return MATCH if stored hash matches', async () => {
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
||||
]);
|
||||
// We can't easily get the expected hash without calling private method or re-implementing logic.
|
||||
// But we can run checkIntegrity once (NEW) to get the hash, then mock FS with that hash.
|
||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||
const resultNew = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/dir',
|
||||
);
|
||||
const currentHash = resultNew.hash;
|
||||
|
||||
mockFs.readFile.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
'project:id': currentHash,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/dir',
|
||||
);
|
||||
expect(result.status).toBe(IntegrityStatus.MATCH);
|
||||
expect(result.hash).toBe(currentHash);
|
||||
expect(result.fileCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should return MISMATCH if stored hash differs', async () => {
|
||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
||||
]);
|
||||
const resultNew = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/dir',
|
||||
);
|
||||
const currentHash = resultNew.hash;
|
||||
|
||||
mockFs.readFile.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
'project:id': 'different_hash',
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/dir',
|
||||
);
|
||||
expect(result.status).toBe(IntegrityStatus.MISMATCH);
|
||||
expect(result.hash).toBe(currentHash);
|
||||
expect(result.fileCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should result in different hash if filename changes', async () => {
|
||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
||||
]);
|
||||
const result1 = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/project/policies',
|
||||
);
|
||||
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/b.toml', content: 'contentA' },
|
||||
]);
|
||||
const result2 = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/project/policies',
|
||||
);
|
||||
|
||||
expect(result1.hash).not.toBe(result2.hash);
|
||||
});
|
||||
|
||||
it('should result in different hash if content changes', async () => {
|
||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
||||
]);
|
||||
const result1 = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/project/policies',
|
||||
);
|
||||
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/a.toml', content: 'contentB' },
|
||||
]);
|
||||
const result2 = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/project/policies',
|
||||
);
|
||||
|
||||
expect(result1.hash).not.toBe(result2.hash);
|
||||
});
|
||||
|
||||
it('should be deterministic (sort order)', async () => {
|
||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
||||
{ path: '/project/policies/b.toml', content: 'contentB' },
|
||||
]);
|
||||
const result1 = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/project/policies',
|
||||
);
|
||||
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/project/policies/b.toml', content: 'contentB' },
|
||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
||||
]);
|
||||
const result2 = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'id',
|
||||
'/project/policies',
|
||||
);
|
||||
|
||||
expect(result1.hash).toBe(result2.hash);
|
||||
});
|
||||
|
||||
it('should handle multiple projects correctly', async () => {
|
||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||
|
||||
// First, get hashes for two different projects
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/dirA/p.toml', content: 'contentA' },
|
||||
]);
|
||||
const { hash: hashA } = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'idA',
|
||||
'/dirA',
|
||||
);
|
||||
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/dirB/p.toml', content: 'contentB' },
|
||||
]);
|
||||
const { hash: hashB } = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'idB',
|
||||
'/dirB',
|
||||
);
|
||||
|
||||
// Now mock storage with both
|
||||
mockFs.readFile.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
'project:idA': hashA,
|
||||
'project:idB': 'oldHashB', // Different from hashB
|
||||
}),
|
||||
);
|
||||
|
||||
// Project A should match
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/dirA/p.toml', content: 'contentA' },
|
||||
]);
|
||||
const resultA = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'idA',
|
||||
'/dirA',
|
||||
);
|
||||
expect(resultA.status).toBe(IntegrityStatus.MATCH);
|
||||
expect(resultA.hash).toBe(hashA);
|
||||
|
||||
// Project B should mismatch
|
||||
readPolicyFilesMock.mockResolvedValue([
|
||||
{ path: '/dirB/p.toml', content: 'contentB' },
|
||||
]);
|
||||
const resultB = await integrityManager.checkIntegrity(
|
||||
'project',
|
||||
'idB',
|
||||
'/dirB',
|
||||
);
|
||||
expect(resultB.status).toBe(IntegrityStatus.MISMATCH);
|
||||
expect(resultB.hash).toBe(hashB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('acceptIntegrity', () => {
|
||||
it('should save the hash to storage', async () => {
|
||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); // Start empty
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
await integrityManager.acceptIntegrity('project', 'id', 'hash123');
|
||||
|
||||
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
||||
'/mock/storage/policy_integrity.json',
|
||||
JSON.stringify({ 'project:id': 'hash123' }, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update existing hash', async () => {
|
||||
mockFs.readFile.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
'other:id': 'otherhash',
|
||||
}),
|
||||
);
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
await integrityManager.acceptIntegrity('project', 'id', 'hash123');
|
||||
|
||||
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
||||
'/mock/storage/policy_integrity.json',
|
||||
JSON.stringify(
|
||||
{
|
||||
'other:id': 'otherhash',
|
||||
'project:id': 'hash123',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as crypto from 'node:crypto';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { readPolicyFiles } from './toml-loader.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
export enum IntegrityStatus {
|
||||
MATCH = 'MATCH',
|
||||
MISMATCH = 'MISMATCH',
|
||||
NEW = 'NEW',
|
||||
}
|
||||
|
||||
export interface IntegrityResult {
|
||||
status: IntegrityStatus;
|
||||
hash: string;
|
||||
fileCount: number;
|
||||
}
|
||||
|
||||
interface StoredIntegrityData {
|
||||
[key: string]: string; // key = scope:identifier, value = hash
|
||||
}
|
||||
|
||||
export class PolicyIntegrityManager {
|
||||
/**
|
||||
* Checks the integrity of policies in a given directory against the stored hash.
|
||||
*
|
||||
* @param scope The scope of the policy (e.g., 'project', 'user').
|
||||
* @param identifier A unique identifier for the policy scope (e.g., project path).
|
||||
* @param policyDir The directory containing the policy files.
|
||||
* @returns IntegrityResult indicating if the current policies match the stored hash.
|
||||
*/
|
||||
async checkIntegrity(
|
||||
scope: string,
|
||||
identifier: string,
|
||||
policyDir: string,
|
||||
): Promise<IntegrityResult> {
|
||||
const { hash: currentHash, fileCount } =
|
||||
await PolicyIntegrityManager.calculateIntegrityHash(policyDir);
|
||||
const storedData = await this.loadIntegrityData();
|
||||
const key = this.getIntegrityKey(scope, identifier);
|
||||
const storedHash = storedData[key];
|
||||
|
||||
if (!storedHash) {
|
||||
return { status: IntegrityStatus.NEW, hash: currentHash, fileCount };
|
||||
}
|
||||
|
||||
if (storedHash === currentHash) {
|
||||
return { status: IntegrityStatus.MATCH, hash: currentHash, fileCount };
|
||||
}
|
||||
|
||||
return { status: IntegrityStatus.MISMATCH, hash: currentHash, fileCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts and persists the current integrity hash for a given policy scope.
|
||||
*
|
||||
* @param scope The scope of the policy.
|
||||
* @param identifier A unique identifier for the policy scope (e.g., project path).
|
||||
* @param hash The hash to persist.
|
||||
*/
|
||||
async acceptIntegrity(
|
||||
scope: string,
|
||||
identifier: string,
|
||||
hash: string,
|
||||
): Promise<void> {
|
||||
const storedData = await this.loadIntegrityData();
|
||||
const key = this.getIntegrityKey(scope, identifier);
|
||||
storedData[key] = hash;
|
||||
await this.saveIntegrityData(storedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a SHA-256 hash of all policy files in the directory.
|
||||
* The hash includes the relative file path and content to detect renames and modifications.
|
||||
*
|
||||
* @param policyDir The directory containing the policy files.
|
||||
* @returns The calculated hash and file count
|
||||
*/
|
||||
private static async calculateIntegrityHash(
|
||||
policyDir: string,
|
||||
): Promise<{ hash: string; fileCount: number }> {
|
||||
try {
|
||||
const files = await readPolicyFiles(policyDir);
|
||||
|
||||
// Sort files by path to ensure deterministic hashing
|
||||
files.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(policyDir, file.path);
|
||||
// Include relative path and content in the hash
|
||||
hash.update(relativePath);
|
||||
hash.update('\0'); // Separator
|
||||
hash.update(file.content);
|
||||
hash.update('\0'); // Separator
|
||||
}
|
||||
|
||||
return { hash: hash.digest('hex'), fileCount: files.length };
|
||||
} catch (error) {
|
||||
debugLogger.error('Failed to calculate policy integrity hash', error);
|
||||
// Return a unique hash (random) to force a mismatch if calculation fails?
|
||||
// Or throw? Throwing is better so we don't accidentally accept/deny corrupted state.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private getIntegrityKey(scope: string, identifier: string): string {
|
||||
return `${scope}:${identifier}`;
|
||||
}
|
||||
|
||||
private async loadIntegrityData(): Promise<StoredIntegrityData> {
|
||||
const storagePath = Storage.getPolicyIntegrityStoragePath();
|
||||
try {
|
||||
const content = await fs.readFile(storagePath, 'utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return JSON.parse(content) as StoredIntegrityData;
|
||||
} catch (error) {
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
(error as Record<string, unknown>)['code'] === 'ENOENT'
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
debugLogger.error('Failed to load policy integrity data', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private async saveIntegrityData(data: StoredIntegrityData): Promise<void> {
|
||||
const storagePath = Storage.getPolicyIntegrityStoragePath();
|
||||
try {
|
||||
await fs.mkdir(path.dirname(storagePath), { recursive: true });
|
||||
await fs.writeFile(storagePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
} catch (error) {
|
||||
debugLogger.error('Failed to save policy integrity data', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,53 @@ export interface PolicyLoadResult {
|
||||
errors: PolicyFileError[];
|
||||
}
|
||||
|
||||
export interface PolicyFile {
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads policy files from a directory or a single file.
|
||||
*
|
||||
* @param policyPath Path to a directory or a .toml file.
|
||||
* @returns Array of PolicyFile objects.
|
||||
*/
|
||||
export async function readPolicyFiles(
|
||||
policyPath: string,
|
||||
): Promise<PolicyFile[]> {
|
||||
let filesToLoad: string[] = [];
|
||||
let baseDir = '';
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(policyPath);
|
||||
if (stats.isDirectory()) {
|
||||
baseDir = policyPath;
|
||||
const dirEntries = await fs.readdir(policyPath, { withFileTypes: true });
|
||||
filesToLoad = dirEntries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.toml'))
|
||||
.map((entry) => entry.name);
|
||||
} else if (stats.isFile() && policyPath.endsWith('.toml')) {
|
||||
baseDir = path.dirname(policyPath);
|
||||
filesToLoad = [path.basename(policyPath)];
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const error = e as NodeJS.ErrnoException;
|
||||
if (error.code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const results: PolicyFile[] = [];
|
||||
for (const file of filesToLoad) {
|
||||
const filePath = path.join(baseDir, file);
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
results.push({ path: filePath, content });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a tier number to a human-readable tier name.
|
||||
*/
|
||||
@@ -227,30 +274,13 @@ export async function loadPoliciesFromToml(
|
||||
const tier = getPolicyTier(p);
|
||||
const tierName = getTierName(tier);
|
||||
|
||||
let filesToLoad: string[] = [];
|
||||
let baseDir = '';
|
||||
let policyFiles: PolicyFile[] = [];
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(p);
|
||||
if (stats.isDirectory()) {
|
||||
baseDir = p;
|
||||
const dirEntries = await fs.readdir(p, { withFileTypes: true });
|
||||
filesToLoad = dirEntries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.toml'))
|
||||
.map((entry) => entry.name);
|
||||
} else if (stats.isFile() && p.endsWith('.toml')) {
|
||||
baseDir = path.dirname(p);
|
||||
filesToLoad = [path.basename(p)];
|
||||
}
|
||||
// Other file types or non-.toml files are silently ignored
|
||||
// for consistency with directory scanning behavior.
|
||||
policyFiles = await readPolicyFiles(p);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const error = e as NodeJS.ErrnoException;
|
||||
if (error.code === 'ENOENT') {
|
||||
// Path doesn't exist, skip it (not an error)
|
||||
continue;
|
||||
}
|
||||
errors.push({
|
||||
filePath: p,
|
||||
fileName: path.basename(p),
|
||||
@@ -262,13 +292,10 @@ export async function loadPoliciesFromToml(
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const file of filesToLoad) {
|
||||
const filePath = path.join(baseDir, file);
|
||||
for (const { path: filePath, content: fileContent } of policyFiles) {
|
||||
const file = path.basename(filePath);
|
||||
|
||||
try {
|
||||
// Read file
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
// Parse TOML
|
||||
let parsed: unknown;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user