secanix 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,8 @@ Checks it runs:
9
9
  - Disabled Supabase Row Level Security (RLS)
10
10
  - CORS wildcard origins
11
11
  - Vulnerable dependencies (known CVEs)
12
+ - Open Firebase security rules (Firestore/Realtime Database/Storage)
13
+ - Exposed Firebase Admin SDK keys (env leak or a committed service account JSON file)
12
14
 
13
15
  ## CLI Usage
14
16
 
@@ -0,0 +1,59 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, extname, relative, sep } from "node:path";
3
+ import { collectFiles, SOURCE_EXTENSIONS } from "./fsWalk.js";
4
+ // NEXT_PUBLIC_* env vars get inlined into the client bundle at build time —
5
+ // a Firebase Admin SDK key under that prefix is a guaranteed leak.
6
+ const PUBLIC_ADMIN_KEY_PATTERN = /NEXT_PUBLIC_[A-Z0-9_]*(?:FIREBASE_PRIVATE_KEY|FIREBASE_ADMIN|SERVICE_ACCOUNT)[A-Z0-9_]*/i;
7
+ // The literal downloaded Admin SDK key file always carries this exact field.
8
+ // gitleaks' generic PEM detector expects real newlines, not the \n-escaped
9
+ // key JSON.stringify produces, so a committed key file slips past secret-scan.
10
+ const SERVICE_ACCOUNT_JSON_PATTERN = /"type"\s*:\s*"service_account"/;
11
+ function isScannableFile(filePath) {
12
+ const name = basename(filePath);
13
+ if (name.startsWith(".env"))
14
+ return true;
15
+ const ext = extname(filePath);
16
+ return SOURCE_EXTENSIONS.has(ext) || ext === ".json";
17
+ }
18
+ export function scanTextForPublicFirebaseAdminKey(content) {
19
+ const matches = [];
20
+ content.split(/\r?\n/).forEach((line, index) => {
21
+ const match = PUBLIC_ADMIN_KEY_PATTERN.exec(line);
22
+ if (match)
23
+ matches.push({ line: index + 1, variableName: match[0] });
24
+ });
25
+ return matches;
26
+ }
27
+ export function scanTextForCommittedServiceAccountKey(content) {
28
+ const matches = [];
29
+ content.split(/\r?\n/).forEach((line, index) => {
30
+ if (SERVICE_ACCOUNT_JSON_PATTERN.test(line))
31
+ matches.push({ line: index + 1 });
32
+ });
33
+ return matches;
34
+ }
35
+ export async function findExposedFirebaseAdminKeys(targetDir) {
36
+ const files = await collectFiles(targetDir, isScannableFile);
37
+ const findings = [];
38
+ for (const file of files) {
39
+ const content = await readFile(file, "utf8");
40
+ const relFile = relative(targetDir, file).split(sep).join("/");
41
+ for (const match of scanTextForPublicFirebaseAdminKey(content)) {
42
+ findings.push({
43
+ file: relFile,
44
+ line: match.line,
45
+ ruleId: "firebase-admin-key-public-env",
46
+ description: `Env var publik "${match.variableName}" kelihatan nyimpen Firebase Admin SDK key — Next.js bakal inline ini ke client bundle.`,
47
+ });
48
+ }
49
+ for (const match of scanTextForCommittedServiceAccountKey(content)) {
50
+ findings.push({
51
+ file: relFile,
52
+ line: match.line,
53
+ ruleId: "firebase-service-account-key-committed",
54
+ description: `File ini kelihatan kayak service account key JSON asli dari Firebase Admin SDK — private key literal ke-commit ke repo.`,
55
+ });
56
+ }
57
+ }
58
+ return findings;
59
+ }
@@ -0,0 +1,46 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, relative, sep } from "node:path";
3
+ import { collectFiles } from "./fsWalk.js";
4
+ const OPEN_SECURITY_RULE_PATTERN = /allow\s+[\w,\s]+:\s*if\s+true\s*;/gi;
5
+ const OPEN_DATABASE_JSON_PATTERN = /"\.(?:read|write)"\s*:\s*(?:true|"true")/gi;
6
+ function isFirebaseRulesFile(filePath) {
7
+ const name = basename(filePath);
8
+ return name === "firestore.rules" || name === "storage.rules" || name === "database.rules.json";
9
+ }
10
+ function findMatchesAcrossLines(content, globalPattern) {
11
+ const matches = [];
12
+ const pattern = new RegExp(globalPattern.source, globalPattern.flags);
13
+ let match;
14
+ while ((match = pattern.exec(content)) !== null) {
15
+ const line = content.slice(0, match.index).split(/\r?\n/).length;
16
+ matches.push({ line });
17
+ }
18
+ return matches;
19
+ }
20
+ export function findOpenRulesInSecurityRulesText(content) {
21
+ return findMatchesAcrossLines(content, OPEN_SECURITY_RULE_PATTERN);
22
+ }
23
+ export function findOpenRulesInDatabaseJson(content) {
24
+ return findMatchesAcrossLines(content, OPEN_DATABASE_JSON_PATTERN);
25
+ }
26
+ export async function findOpenFirebaseRules(targetDir) {
27
+ const files = await collectFiles(targetDir, isFirebaseRulesFile);
28
+ const findings = [];
29
+ for (const file of files) {
30
+ const name = basename(file);
31
+ const content = await readFile(file, "utf8");
32
+ const relFile = relative(targetDir, file).split(sep).join("/");
33
+ const matches = name === "database.rules.json"
34
+ ? findOpenRulesInDatabaseJson(content)
35
+ : findOpenRulesInSecurityRulesText(content);
36
+ for (const match of matches) {
37
+ findings.push({
38
+ file: relFile,
39
+ line: match.line,
40
+ ruleId: "firebase-rules-open",
41
+ description: `Rule di "${name}" ngasih akses baca/tulis tanpa syarat (if true / .read atau .write: true) — data bisa diakses siapa aja tanpa auth.`,
42
+ });
43
+ }
44
+ }
45
+ return findings;
46
+ }
package/dist/cli.js CHANGED
@@ -6,7 +6,9 @@ import { pathToFileURL } from "node:url";
6
6
  import { findMissingApiAuth, SemgrepNotFoundError } from "./checks/apiAuthMissing.js";
7
7
  import { findCorsWildcard } from "./checks/corsWildcard.js";
8
8
  import { findDependencyVulnerabilities, OsvScannerNotFoundError } from "./checks/dependencyVulnerabilities.js";
9
+ import { findExposedFirebaseAdminKeys } from "./checks/exposedFirebaseAdminKey.js";
9
10
  import { findExposedServiceRoleKeys } from "./checks/exposedServiceRoleKey.js";
11
+ import { findOpenFirebaseRules } from "./checks/firebaseRulesOpen.js";
10
12
  import { findRlsDisabledTables } from "./checks/rlsDisabled.js";
11
13
  import { GitleaksNotFoundError, runSecretScan } from "./checks/secretScan.js";
12
14
  import { applyIgnoreRules, buildReport, formatReport } from "./report.js";
@@ -76,6 +78,11 @@ export async function run(targetDir = process.cwd(), options = {}) {
76
78
  }
77
79
  checkFindings.push({ checkId: "rls-disabled", findings: await findRlsDisabledTables(targetDir) });
78
80
  checkFindings.push({ checkId: "cors-wildcard", findings: await findCorsWildcard(targetDir) });
81
+ checkFindings.push({ checkId: "firebase-rules-open", findings: await findOpenFirebaseRules(targetDir) });
82
+ checkFindings.push({
83
+ checkId: "firebase-admin-key-exposed",
84
+ findings: await findExposedFirebaseAdminKeys(targetDir),
85
+ });
79
86
  try {
80
87
  checkFindings.push({
81
88
  checkId: "dependency-cve",
package/dist/report.js CHANGED
@@ -20,6 +20,18 @@ const RULE_INFO = {
20
20
  severity: "medium",
21
21
  fix: "Ganti '*' jadi daftar origin eksplisit yang emang butuh akses, atau validasi origin secara dinamis di server.",
22
22
  },
23
+ "firebase-rules-open": {
24
+ severity: "critical",
25
+ fix: "Ganti 'if true' / '.read'/'.write': true dengan rule yang validasi auth (request.auth != null, dst) — rule default Firebase itu deny-all, jangan di-override jadi allow-all.",
26
+ },
27
+ "firebase-admin-key-public-env": {
28
+ severity: "critical",
29
+ fix: "Jangan expose Firebase Admin key lewat NEXT_PUBLIC_* — pindahin ke env var server-only, lalu rotate key ini di Firebase Console (Project Settings > Service Accounts) kalau udah sempet ke-deploy.",
30
+ },
31
+ "firebase-service-account-key-committed": {
32
+ severity: "critical",
33
+ fix: "Hapus file JSON key ini dari repo & git history, rotate key di Firebase Console, generate key baru & simpan di secret manager / env var server-only.",
34
+ },
23
35
  };
24
36
  // gitleaks (secret-scan) and osv-scanner (dependency-cve) each emit a
25
37
  // ruleId that varies per finding (rule name / GHSA id), so they're
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secanix",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "description": "Security scanner buat app hasil vibe-coding (Next.js + Supabase/Firebase).",
6
6
  "license": "MIT",