← back to Rentv Adintel

scripts/audit-source-policies.js

162 lines

#!/usr/bin/env node
'use strict';
/**
 * CLI: node scripts/audit-source-policies.js
 *
 * Loads all source_policies from the database, runs
 * lib/compliance/source-policy.validateSourcePolicy on each, and prints a
 * PASS/FAIL table. Exits with code 1 if any ENABLED policy is illegal.
 *
 * Also validates the structural integrity of each policy regardless of its
 * enabled state so developers can catch problems before enabling.
 *
 * @module scripts/audit-source-policies
 */

require('../lib/env');
const { pool, query } = require('../db');

let validateSourcePolicy;
let assertSourceEnabledLegal;
try {
  ({ validateSourcePolicy, assertSourceEnabledLegal } =
    require('../lib/compliance/source-policy'));
} catch (err) {
  console.error('[audit-sources] Could not load source-policy validator:', err.message);
  process.exit(1);
}

/**
 * Map DB row snake_case to the JS camelCase shape expected by validateSourcePolicy.
 * @param {Object} row
 * @returns {Object}
 */
function rowToPolicy(row) {
  return {
    sourceKey:              row.source_key,
    displayName:            row.display_name,
    owner:                  row.owner,
    baseUrl:                row.base_url,
    accessMethod:           row.access_method,
    allowsAutomatedAccess:  row.allows_automated_access,
    allowsScreenshotCapture: row.allows_screenshot_capture,
    allowsInternalStorage:  row.allows_internal_storage,
    allowsExport:           row.allows_export,
    prohibitedHosts:        row.prohibited_hosts,
    permittedPaths:         row.permitted_paths,
    prohibitedPaths:        row.prohibited_paths,
    maxRequestsPerMinute:   row.max_requests_per_minute,
    minimumDelayMs:         row.minimum_delay_ms,
    retentionDays:          row.retention_days,
    enabled:                row.enabled,
    reviewedAt:             row.reviewed_at,
    reviewNotes:            row.review_notes,
  };
}

(async () => {
  let exitCode = 0;

  try {
    const res = await query(
      `SELECT * FROM source_policies ORDER BY enabled DESC, display_name`
    );
    const rows = res.rows;

    if (rows.length === 0) {
      console.log('[audit-sources] No source policies found in the database.');
      console.log('[audit-sources] Run db:seed to load initial policies.');
      process.exit(0);
    }

    // Table header
    const COL_KEY    = 36;
    const COL_ENABLED = 9;
    const COL_RESULT = 8;

    const hr = '-'.repeat(COL_KEY + COL_ENABLED + COL_RESULT + 40);
    console.log('\nSource Policy Audit');
    console.log(hr);
    console.log(
      'SOURCE_KEY'.padEnd(COL_KEY) +
      'ENABLED'.padEnd(COL_ENABLED) +
      'RESULT'.padEnd(COL_RESULT) +
      'NOTES'
    );
    console.log(hr);

    const enabledIllegal = [];
    const allResults = [];

    for (const row of rows) {
      const policy = rowToPolicy(row);
      const { valid, errors } = validateSourcePolicy(policy);

      // Also check the harder assert for enabled policies
      let assertErr = null;
      if (policy.enabled) {
        try {
          assertSourceEnabledLegal(policy);
        } catch (e) {
          assertErr = e.message;
          valid === false; // already false if errors exist
        }
      }

      const isIllegal = !valid || !!assertErr;
      const result = isIllegal ? 'FAIL' : 'PASS';

      if (isIllegal && policy.enabled) {
        enabledIllegal.push({ policy, errors, assertErr });
      }

      const notes = [
        ...errors,
        assertErr ? `[ASSERT] ${assertErr}` : null,
      ].filter(Boolean).join('; ') || (valid ? 'OK' : 'see errors');

      const resultTag = isIllegal ? `\x1b[31m${result}\x1b[0m` : `\x1b[32m${result}\x1b[0m`;

      console.log(
        (policy.sourceKey || '(no key)').slice(0, COL_KEY - 1).padEnd(COL_KEY) +
        String(policy.enabled).padEnd(COL_ENABLED) +
        resultTag.padEnd(COL_RESULT + 10) + // +10 for ANSI codes
        notes.slice(0, 120)
      );

      allResults.push({ sourceKey: policy.sourceKey, valid, errors, enabled: policy.enabled });
    }

    console.log(hr);
    const passed = allResults.filter((r) => r.valid).length;
    const failed = allResults.length - passed;
    const enabledFailed = enabledIllegal.length;

    console.log(`\nSummary: ${rows.length} policies — ${passed} PASS, ${failed} FAIL`);

    if (enabledIllegal.length > 0) {
      console.error(`\n\x1b[31mERROR: ${enabledFailed} ENABLED policy/policies are ILLEGAL (spec §6):\x1b[0m`);
      for (const { policy, errors, assertErr } of enabledIllegal) {
        console.error(`  SOURCE: ${policy.sourceKey}`);
        for (const e of errors) console.error(`    - ${e}`);
        if (assertErr) console.error(`    - [ASSERT] ${assertErr}`);
      }
      console.error('\nDisable or correct these policies before enabling research automation.');
      exitCode = 1;
    } else if (failed > 0) {
      console.log(`\n${failed} policy/policies have validation issues but are not enabled.`);
      console.log('No action required until you enable them.');
    } else {
      console.log('\nAll enabled policies are compliant with spec §6.');
    }

  } catch (err) {
    console.error('[audit-sources] Fatal error:', err.message);
    console.error(err.stack);
    exitCode = 1;
  } finally {
    try { await pool.end(); } catch (_) {}
    process.exit(exitCode);
  }
})();