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
41 changes: 10 additions & 31 deletions .audit-credentials-baseline.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,12 @@
{
"generated_at": "2026-05-26T12:06:33.381Z",
"schema": "v2-counts",
"issue_count": 28,
"signatures": {
"supabase/functions/categories-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1,
"supabase/functions/categories-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1,
"supabase/functions/cleanup-novelties/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1,
"supabase/functions/cleanup-novelties/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1,
"supabase/functions/cnpj-lookup/index.ts:ssot-bypass:SIMULATION_BYPASS_KEY": 1,
"supabase/functions/expert-chat/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 2,
"supabase/functions/expert-chat/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 2,
"supabase/functions/external-db-inspect/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1,
"supabase/functions/external-db-inspect/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1,
"supabase/functions/health-check/index.ts:module-scope-credential-read:EXTERNAL_SUPABASE_SERVICE_KEY": 1,
"supabase/functions/health-check/index.ts:module-scope-credential-read:EXTERNAL_SUPABASE_URL": 1,
"supabase/functions/health-check/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1,
"supabase/functions/health-check/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1,
"supabase/functions/materials-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1,
"supabase/functions/materials-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_ROLE_KEY": 1,
"supabase/functions/materials-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1,
"supabase/functions/product-webhook/index.ts:module-scope-credential-read:N8N_PRODUCT_WEBHOOK_SECRET": 1,
"supabase/functions/product-webhook/index.ts:ssot-bypass:N8N_PRODUCT_WEBHOOK_SECRET": 1,
"supabase/functions/product-webhook/index.ts:ssot-bypass:N8N_PRODUCT_WEBHOOK_TOLERANCE_SEC": 1,
"supabase/functions/secure-upload/index.ts:ssot-bypass:VIRUSTOTAL_API_KEY": 1,
"supabase/functions/send-scheduled-reports/index.ts:ssot-bypass:RESEND_API_KEY": 1,
"supabase/functions/simulation-orchestrator/index.ts:ssot-bypass:N8N_PRODUCT_WEBHOOK_SECRET": 1,
"supabase/functions/sync-external-db/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1,
"supabase/functions/sync-external-db/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_ROLE_KEY": 1,
"supabase/functions/sync-external-db/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1,
"supabase/functions/webhook-dispatcher/index.ts:ssot-bypass:WEBHOOK_DISPATCHER_SECRET": 1
}
"generated_at": "2026-05-26T15:49:16.936Z",
"issue_count": 6,
"issues": [
"supabase/functions/categories-api/index.ts:28:ssot-bypass:EXTERNAL_SUPABASE_URL",
"supabase/functions/categories-api/index.ts:29:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY",
"supabase/functions/cleanup-novelties/index.ts:38:ssot-bypass:EXTERNAL_SUPABASE_URL",
"supabase/functions/cleanup-novelties/index.ts:39:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY",
"supabase/functions/external-db-inspect/index.ts:78:ssot-bypass:EXTERNAL_SUPABASE_URL",
"supabase/functions/external-db-inspect/index.ts:79:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY"
]
}
469 changes: 327 additions & 142 deletions src/pages/mockups/MockupGenerator.tsx

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions supabase/functions/expert-chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { z } from 'npm:zod@3.23.8';
import { callAiWithTracking, QuotaExceededError } from '../_shared/ai-usage.ts';
import { rateLimiters, applyRateLimit } from '../_shared/rate-limiter.ts';
import { runBotProtection } from '../_shared/bot-protection.ts';
import { resolveCredential } from '../_shared/credentials.ts';
import { resolveCredential, getCredential } from '../_shared/credentials.ts';
import { extractAndParseAIJSON, safeJson } from '../_shared/json-parser.ts';
import { safeErrorFields } from '../_shared/log-safety.ts';
import { assertSwitchEnabled } from '../_shared/kill_switch.ts';
Expand Down Expand Up @@ -998,8 +998,9 @@ ${
let productsContext = '';
let semanticResults: any[] = [];

const EXT_URL = Deno.env.get('EXTERNAL_SUPABASE_URL');
const EXT_KEY = Deno.env.get('EXTERNAL_SUPABASE_SERVICE_KEY');
// fix: ssot-bypass — credential vault
const EXT_URL = await getCredential('EXTERNAL_PROMOBRIND_URL');
const EXT_KEY = await getCredential('EXTERNAL_PROMOBRIND_SERVICE_ROLE_KEY');

if (!EXT_URL || !EXT_KEY) {
console.error('External DB env vars not set — cannot fetch products');
Expand Down Expand Up @@ -1279,8 +1280,9 @@ ${generalPool.map((p) => buildProductDescription(p)).join('\n\n')}
`;
}
} else {
const EXT_URL2 = Deno.env.get('EXTERNAL_SUPABASE_URL');
const EXT_KEY2 = Deno.env.get('EXTERNAL_SUPABASE_SERVICE_KEY');
// fix: ssot-bypass — credential vault
const EXT_URL2 = await getCredential('EXTERNAL_PROMOBRIND_URL');
const EXT_KEY2 = await getCredential('EXTERNAL_PROMOBRIND_SERVICE_ROLE_KEY');
if (EXT_URL2 && EXT_KEY2) {
const extClient = createClient(EXT_URL2, EXT_KEY2);
let q = extClient
Expand Down
24 changes: 14 additions & 10 deletions supabase/functions/product-webhook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { crypto } from 'https://deno.land/std@0.224.0/crypto/mod.ts';
import { encodeHex } from 'https://deno.land/std@0.224.0/encoding/hex.ts';
import { buildPublicCorsHeaders } from '../_shared/cors.ts';
import { parseContract } from '../_shared/contracts/index.ts';
import { getCredential } from '../_shared/credentials.ts';
import {
ProductWebhookSchemas,
type ProductWebhookV1Payload,
Expand All @@ -11,21 +12,13 @@ import {

const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const webhookSecret = Deno.env.get('N8N_PRODUCT_WEBHOOK_SECRET');
const configuredBatchSize = Number(Deno.env.get('PRODUCT_WEBHOOK_BATCH_SIZE') ?? '200');
const MAX_BATCH_SIZE = 500;
const BATCH_SIZE = Number.isFinite(configuredBatchSize)
? Math.min(Math.max(Math.trunc(configuredBatchSize), 100), MAX_BATCH_SIZE)
: 200;
const DEFAULT_WEBHOOK_TOLERANCE_SEC = 300;
const MAX_WEBHOOK_TOLERANCE_SEC = 3600;
const configuredWebhookToleranceSec = Number(
Deno.env.get('N8N_PRODUCT_WEBHOOK_TOLERANCE_SEC') ?? DEFAULT_WEBHOOK_TOLERANCE_SEC,
);
const webhookTimestampToleranceSec =
Number.isFinite(configuredWebhookToleranceSec) && configuredWebhookToleranceSec > 0
? Math.min(Math.floor(configuredWebhookToleranceSec), MAX_WEBHOOK_TOLERANCE_SEC)
: DEFAULT_WEBHOOK_TOLERANCE_SEC;

const allowedOrigins = new Set(
(Deno.env.get('PRODUCT_WEBHOOK_ALLOWED_ORIGINS') ?? '')
Expand Down Expand Up @@ -151,8 +144,9 @@ async function isReplayNonce(
supabase: SupabaseClient<any>,
nonce: string,
timestamp: number,
toleranceSec: number,
): Promise<boolean> {
const expiresAt = new Date((timestamp + webhookTimestampToleranceSec) * 1000).toISOString();
const expiresAt = new Date((timestamp + toleranceSec) * 1000).toISOString();

const { error } = await supabase.from('webhook_request_nonces' as never).insert({
source: 'product-webhook',
Expand Down Expand Up @@ -195,6 +189,16 @@ Deno.serve(async (req) => {
try {
const rawBody = await req.text();

// fix: ssot-bypass + module-scope-credential-read — read from credential vault per-request
const webhookSecret = await getCredential('N8N_PRODUCT_WEBHOOK_SECRET');
const configuredWebhookToleranceSec = Number(
await getCredential('N8N_PRODUCT_WEBHOOK_TOLERANCE_SEC') ?? DEFAULT_WEBHOOK_TOLERANCE_SEC,
);
const webhookTimestampToleranceSec =
Number.isFinite(configuredWebhookToleranceSec) && configuredWebhookToleranceSec > 0
? Math.min(Math.floor(configuredWebhookToleranceSec), MAX_WEBHOOK_TOLERANCE_SEC)
: DEFAULT_WEBHOOK_TOLERANCE_SEC;

if (!webhookSecret) {
logAuthFailure('misconfigured_secret', req);
return new Response(UNAUTHORIZED_BODY, {
Expand Down Expand Up @@ -249,7 +253,7 @@ Deno.serve(async (req) => {
});
}

const replayed = await isReplayNonce(supabase, nonce, timestamp);
const replayed = await isReplayNonce(supabase, nonce, timestamp, webhookTimestampToleranceSec);
if (replayed) {
logAuthFailure('replayed_nonce', req, { nonceSize: nonce.length });
return new Response(UNAUTHORIZED_BODY, {
Expand Down
4 changes: 3 additions & 1 deletion supabase/functions/secure-upload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStructuredLogger } from "../_shared/structured-logger.ts";
import { getOrCreateRequestId } from "../_shared/request-id.ts";
import { buildPublicCorsHeaders } from "../_shared/cors.ts";
import { safeErrorResponse } from "../_shared/error-response.ts";
import { getCredential } from "../_shared/credentials.ts";

const corsHeaders = buildPublicCorsHeaders();

Expand Down Expand Up @@ -71,7 +72,8 @@ Deno.serve(async (req) => {
};
let targetBucket = "personalization-images";
let targetPrefix = "verified";
const vtApiKey = Deno.env.get("VIRUSTOTAL_API_KEY");
// fix: ssot-bypass — credential vault
const vtApiKey = await getCredential("VIRUSTOTAL_API_KEY");

if (vtApiKey) {
try {
Expand Down
4 changes: 3 additions & 1 deletion supabase/functions/send-scheduled-reports/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4";
import { buildPublicCorsHeaders } from "../_shared/cors.ts";
import { authorizeCron } from "../_shared/dispatcher-auth.ts";
import { getCredential } from "../_shared/credentials.ts";

const corsHeaders = buildPublicCorsHeaders();

Expand All @@ -20,7 +21,8 @@ Deno.serve(async (req: Request) => {
try {
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const resendKey = Deno.env.get("RESEND_API_KEY");
// fix: ssot-bypass — credential vault
const resendKey = await getCredential("RESEND_API_KEY");
const supabase = createClient(supabaseUrl, serviceKey);

// Find reports due to run
Expand Down
4 changes: 3 additions & 1 deletion supabase/functions/simulation-orchestrator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SimulationOrchestratorSchemas,
} from "../_shared/contracts/schemas/simulation-orchestrator.ts";
import { buildPublicCorsHeaders } from "../_shared/cors.ts";
import { getCredential } from "../_shared/credentials.ts";

const corsHeaders = buildPublicCorsHeaders();

Expand Down Expand Up @@ -59,7 +60,8 @@ Deno.serve(async (req) => {

const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const n8nSecret = Deno.env.get("N8N_PRODUCT_WEBHOOK_SECRET") || "sim-secret";
// fix: ssot-bypass — credential vault
const n8nSecret = await getCredential("N8N_PRODUCT_WEBHOOK_SECRET") ?? "sim-secret";
Comment on lines +63 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remover fallback de segredo hardcoded em Line 64.

?? "sim-secret" introduz segredo previsível quando o vault falha/ausenta credencial, enfraquecendo a proteção do webhook. Falhe fechado com 503 em vez de usar default secreto.

💡 Patch sugerido
-    const n8nSecret = await getCredential("N8N_PRODUCT_WEBHOOK_SECRET") ?? "sim-secret";
+    const n8nSecret = await getCredential("N8N_PRODUCT_WEBHOOK_SECRET");
+    if (!n8nSecret) {
+      return new Response(JSON.stringify({ error: "service_misconfigured" }), {
+        status: 503,
+        headers: { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" },
+      });
+    }

As per coding guidelines "Secrets sempre via Deno.env.get(), NUNCA hardcoded".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/simulation-orchestrator/index.ts` around lines 63 - 64,
Remove the hardcoded fallback by deleting the "?? 'sim-secret'" default; call
getCredential("N8N_PRODUCT_WEBHOOK_SECRET") (or
Deno.env.get("N8N_PRODUCT_WEBHOOK_SECRET") per guidelines) and if it returns
undefined, fail closed by returning/throwing a 503 response (log the missing
secret) instead of using a predictable secret; update the n8nSecret usage sites
to assume a present secret after this check (ref: n8nSecret variable and
getCredential call).

const supabase = createClient(supabaseUrl, serviceRoleKey);

const { data: run } = await supabase
Expand Down
6 changes: 4 additions & 2 deletions supabase/functions/sync-external-db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SyncExternalDbSchemas } from "../_shared/contracts/schemas/sync-externa
import { buildPublicCorsHeaders } from "../_shared/cors.ts";
import { authorizeCron } from "../_shared/dispatcher-auth.ts";
import { getOrCreateRequestId } from "../_shared/request-id.ts";
import { getCredential } from "../_shared/credentials.ts";

const corsHeaders = buildPublicCorsHeaders();

Expand Down Expand Up @@ -52,8 +53,9 @@ Deno.serve(async (req) => {
const internalSupabase = createClient(internalUrl, internalKey);

// 2. Conexao com Supabase Externo
const externalUrl = Deno.env.get("EXTERNAL_SUPABASE_URL") || Deno.env.get("VITE_EXTERNAL_SUPABASE_URL");
const externalKey = Deno.env.get("EXTERNAL_SUPABASE_SERVICE_ROLE_KEY") || Deno.env.get("EXTERNAL_SUPABASE_SERVICE_KEY");
// fix: ssot-bypass — credential vault
const externalUrl = await getCredential('EXTERNAL_PROMOBRIND_URL');
const externalKey = await getCredential('EXTERNAL_PROMOBRIND_SERVICE_ROLE_KEY');

if (!externalUrl || !externalKey) {
log.error("missing_external_config", {});
Expand Down
4 changes: 3 additions & 1 deletion supabase/functions/webhook-dispatcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { WebhookDispatcherSchemas } from "../_shared/contracts/schemas/webhook-d
import { buildPublicCorsHeaders } from "../_shared/cors.ts";
import { authorizeDispatcher } from "../_shared/dispatcher-auth.ts";
import { assertSwitchEnabled } from "../_shared/kill_switch.ts";
import { getCredential } from "../_shared/credentials.ts";

const corsHeaders = buildPublicCorsHeaders({ allowMethods: "POST, OPTIONS" });

Expand Down Expand Up @@ -47,7 +48,8 @@ Deno.serve(async (req) => {
if (killResponse) return killResponse;

// Guard: require X-Dispatcher-Secret to prevent unauthorized invocations
const dispatcherSecret = Deno.env.get("WEBHOOK_DISPATCHER_SECRET");
// fix: ssot-bypass — credential vault
const dispatcherSecret = await getCredential("WEBHOOK_DISPATCHER_SECRET");
if (dispatcherSecret) {
const incoming = req.headers.get("x-dispatcher-secret");
if (!incoming || incoming !== dispatcherSecret) {
Expand Down
Loading