@kinkai.cloud/vibecheck 0.1.6 → 0.2.0
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/dist/cli.js +1545 -402
- package/package.json +12 -2
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __export = (target,
|
|
4
|
-
for (var name in
|
|
5
|
-
__defProp(target, name, { get:
|
|
3
|
+
var __export = (target, all2) => {
|
|
4
|
+
for (var name in all2)
|
|
5
|
+
__defProp(target, name, { get: all2[name], enumerable: true });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// ../../packages/checks/src/types.ts
|
|
9
|
+
var SEVERITIES = ["fatal", "critical", "high", "medium", "low"];
|
|
9
10
|
var NODE_KINDS = [
|
|
10
11
|
"browser",
|
|
11
12
|
"app",
|
|
@@ -17,6 +18,7 @@ var NODE_KINDS = [
|
|
|
17
18
|
"payments",
|
|
18
19
|
"secrets"
|
|
19
20
|
];
|
|
21
|
+
var CATEGORIES = ["secrets", "data", "storage", "auth", "ai", "config"];
|
|
20
22
|
|
|
21
23
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
22
24
|
var external_exports = {};
|
|
@@ -14533,164 +14535,401 @@ function date4(params) {
|
|
|
14533
14535
|
config(en_default());
|
|
14534
14536
|
|
|
14535
14537
|
// ../../packages/graph/src/types.ts
|
|
14536
|
-
var
|
|
14537
|
-
var
|
|
14538
|
-
var
|
|
14539
|
-
var
|
|
14540
|
-
var
|
|
14541
|
-
var
|
|
14538
|
+
var EDGE_TYPES = ["imports", "calls", "contains"];
|
|
14539
|
+
var MAX_NODES = 400;
|
|
14540
|
+
var MAX_EDGES = 800;
|
|
14541
|
+
var MAX_COMMUNITIES = 9;
|
|
14542
|
+
var MAX_FILES_PER_NODE = 12;
|
|
14543
|
+
var MAX_COMMUNITY_FILES = 8;
|
|
14542
14544
|
var MAX_BYTES = 256e3;
|
|
14543
|
-
var
|
|
14545
|
+
var MAX_LABEL = 80;
|
|
14544
14546
|
var MAX_ID = 64;
|
|
14545
|
-
var
|
|
14546
|
-
var
|
|
14547
|
-
var
|
|
14548
|
-
var
|
|
14547
|
+
var MAX_PATH = 180;
|
|
14548
|
+
var MAX_PACKAGES = 32;
|
|
14549
|
+
var MAX_HINT_ROUTES = 80;
|
|
14550
|
+
var MAX_HINT_TABLES = 40;
|
|
14551
|
+
var MAX_HINT_BUCKETS = 24;
|
|
14552
|
+
var MAX_HINT_COLLECTIONS = 24;
|
|
14553
|
+
var EMPTY_DATA = { tables: [], buckets: [], collections: [] };
|
|
14554
|
+
var pathSchema = external_exports.string().min(1).max(MAX_PATH);
|
|
14555
|
+
var nodeSchema = external_exports.object({
|
|
14549
14556
|
id: external_exports.string().min(1).max(MAX_ID),
|
|
14550
14557
|
kind: external_exports.enum(NODE_KINDS),
|
|
14551
|
-
|
|
14552
|
-
|
|
14558
|
+
label: external_exports.string().min(1).max(MAX_LABEL),
|
|
14559
|
+
files: external_exports.array(pathSchema).max(MAX_FILES_PER_NODE)
|
|
14553
14560
|
});
|
|
14554
|
-
var
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
|
|
14561
|
+
var edgeSchema = external_exports.object({
|
|
14562
|
+
from: external_exports.string().min(1).max(MAX_ID),
|
|
14563
|
+
to: external_exports.string().min(1).max(MAX_ID),
|
|
14564
|
+
type: external_exports.enum(EDGE_TYPES)
|
|
14558
14565
|
});
|
|
14559
|
-
var
|
|
14566
|
+
var communitySchema = external_exports.object({
|
|
14560
14567
|
nodeKind: external_exports.enum(NODE_KINDS),
|
|
14561
|
-
|
|
14562
|
-
|
|
14568
|
+
label: external_exports.string().min(1).max(MAX_LABEL),
|
|
14569
|
+
files: external_exports.array(pathSchema).max(MAX_COMMUNITY_FILES)
|
|
14563
14570
|
});
|
|
14564
|
-
var
|
|
14565
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
pacotes: external_exports.array(external_exports.string().min(1).max(MAX_ROTULO)).max(MAX_PACOTES)
|
|
14571
|
+
var tableName = external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/);
|
|
14572
|
+
var bucketName = external_exports.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/);
|
|
14573
|
+
var dataSchema = external_exports.object({
|
|
14574
|
+
tables: external_exports.array(tableName).max(MAX_HINT_TABLES),
|
|
14575
|
+
buckets: external_exports.array(bucketName).max(MAX_HINT_BUCKETS),
|
|
14576
|
+
collections: external_exports.array(tableName).max(MAX_HINT_COLLECTIONS)
|
|
14571
14577
|
});
|
|
14578
|
+
var scanGraphSchema = external_exports.object({
|
|
14579
|
+
version: external_exports.literal(1),
|
|
14580
|
+
advanced: external_exports.boolean(),
|
|
14581
|
+
nodes: external_exports.array(nodeSchema).max(MAX_NODES),
|
|
14582
|
+
edges: external_exports.array(edgeSchema).max(MAX_EDGES),
|
|
14583
|
+
communities: external_exports.array(communitySchema).max(MAX_COMMUNITIES),
|
|
14584
|
+
packages: external_exports.array(external_exports.string().min(1).max(MAX_LABEL)).max(MAX_PACKAGES),
|
|
14585
|
+
data: dataSchema.optional()
|
|
14586
|
+
});
|
|
14587
|
+
var EMPTY_HINTS = {
|
|
14588
|
+
apiRoutes: [],
|
|
14589
|
+
tableNames: [],
|
|
14590
|
+
buckets: [],
|
|
14591
|
+
collections: []
|
|
14592
|
+
};
|
|
14572
14593
|
|
|
14573
|
-
// ../../packages/graph/src/
|
|
14574
|
-
function
|
|
14575
|
-
const
|
|
14594
|
+
// ../../packages/graph/src/hints.ts
|
|
14595
|
+
function uniqueCapped(values, cap) {
|
|
14596
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14597
|
+
const out = [];
|
|
14598
|
+
for (const value of values) {
|
|
14599
|
+
if (out.length >= cap) break;
|
|
14600
|
+
if (seen.has(value)) continue;
|
|
14601
|
+
seen.add(value);
|
|
14602
|
+
out.push(value);
|
|
14603
|
+
}
|
|
14604
|
+
return out;
|
|
14605
|
+
}
|
|
14606
|
+
function routeFromFile(path) {
|
|
14607
|
+
const normal = path.replaceAll("\\", "/");
|
|
14576
14608
|
const app = /(?:^|\/)app\/(.+)\/route\.(t|j)sx?$/i.exec(normal);
|
|
14577
14609
|
if (app?.[1] !== void 0) {
|
|
14578
|
-
const
|
|
14579
|
-
if (
|
|
14580
|
-
return `/${
|
|
14610
|
+
const segments = app[1].split("/").filter((s) => s.length > 0 && !s.startsWith("(") && !s.startsWith("@")).map((s) => s.replace(/^\[+\.\.\.(.+)\]+$/, "*").replace(/^\[+(.+)\]+$/, ":$1"));
|
|
14611
|
+
if (segments.length === 0) return "/api";
|
|
14612
|
+
return `/${segments.join("/")}`;
|
|
14581
14613
|
}
|
|
14582
14614
|
const pages = /(?:^|\/)pages\/api\/(.+)\.(t|j)sx?$/i.exec(normal);
|
|
14583
14615
|
if (pages?.[1] !== void 0) {
|
|
14584
|
-
const
|
|
14585
|
-
return `/api/${
|
|
14616
|
+
const withoutIndex = pages[1].replace(/\/index$/i, "");
|
|
14617
|
+
return `/api/${withoutIndex}`;
|
|
14586
14618
|
}
|
|
14587
14619
|
return null;
|
|
14588
14620
|
}
|
|
14621
|
+
function filesFromGraph(graph) {
|
|
14622
|
+
return [
|
|
14623
|
+
...graph.nodes.flatMap((n) => n.files),
|
|
14624
|
+
...graph.communities.flatMap((c) => c.files)
|
|
14625
|
+
];
|
|
14626
|
+
}
|
|
14627
|
+
function hintsFromGraph(graph) {
|
|
14628
|
+
if (graph === null || graph === void 0 || !graph.advanced) return EMPTY_HINTS;
|
|
14629
|
+
const routes = [];
|
|
14630
|
+
for (const file2 of filesFromGraph(graph)) {
|
|
14631
|
+
const route = routeFromFile(file2);
|
|
14632
|
+
if (route !== null) routes.push(route);
|
|
14633
|
+
}
|
|
14634
|
+
routes.sort((a, b) => a.localeCompare(b));
|
|
14635
|
+
const data = graph.data ?? EMPTY_DATA;
|
|
14636
|
+
return {
|
|
14637
|
+
apiRoutes: uniqueCapped(routes, MAX_HINT_ROUTES),
|
|
14638
|
+
tableNames: uniqueCapped(data.tables, MAX_HINT_TABLES),
|
|
14639
|
+
buckets: uniqueCapped(data.buckets, MAX_HINT_BUCKETS),
|
|
14640
|
+
collections: uniqueCapped(data.collections, MAX_HINT_COLLECTIONS)
|
|
14641
|
+
};
|
|
14642
|
+
}
|
|
14643
|
+
|
|
14644
|
+
// ../../packages/graph/src/metrics.ts
|
|
14645
|
+
function importCycles(graph) {
|
|
14646
|
+
const adjacencia = /* @__PURE__ */ new Map();
|
|
14647
|
+
for (const aresta of graph.edges) {
|
|
14648
|
+
if (aresta.type !== "imports") continue;
|
|
14649
|
+
const vizinhos = adjacencia.get(aresta.from) ?? [];
|
|
14650
|
+
vizinhos.push(aresta.to);
|
|
14651
|
+
adjacencia.set(aresta.from, vizinhos);
|
|
14652
|
+
}
|
|
14653
|
+
const indice = /* @__PURE__ */ new Map();
|
|
14654
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
14655
|
+
const naPilha = /* @__PURE__ */ new Set();
|
|
14656
|
+
const pilha = [];
|
|
14657
|
+
let proximo = 0;
|
|
14658
|
+
let ciclos = 0;
|
|
14659
|
+
for (const raiz of graph.nodes.map((n) => n.id)) {
|
|
14660
|
+
if (indice.has(raiz)) continue;
|
|
14661
|
+
const quadros = [{ no: raiz, vizinho: 0 }];
|
|
14662
|
+
indice.set(raiz, proximo);
|
|
14663
|
+
lowlink.set(raiz, proximo);
|
|
14664
|
+
proximo += 1;
|
|
14665
|
+
pilha.push(raiz);
|
|
14666
|
+
naPilha.add(raiz);
|
|
14667
|
+
while (quadros.length > 0) {
|
|
14668
|
+
const quadro = quadros[quadros.length - 1];
|
|
14669
|
+
if (quadro === void 0) break;
|
|
14670
|
+
const vizinhos = adjacencia.get(quadro.no) ?? [];
|
|
14671
|
+
if (quadro.vizinho < vizinhos.length) {
|
|
14672
|
+
const vizinho = vizinhos[quadro.vizinho];
|
|
14673
|
+
quadro.vizinho += 1;
|
|
14674
|
+
if (!indice.has(vizinho)) {
|
|
14675
|
+
indice.set(vizinho, proximo);
|
|
14676
|
+
lowlink.set(vizinho, proximo);
|
|
14677
|
+
proximo += 1;
|
|
14678
|
+
pilha.push(vizinho);
|
|
14679
|
+
naPilha.add(vizinho);
|
|
14680
|
+
quadros.push({ no: vizinho, vizinho: 0 });
|
|
14681
|
+
} else if (naPilha.has(vizinho)) {
|
|
14682
|
+
lowlink.set(
|
|
14683
|
+
quadro.no,
|
|
14684
|
+
Math.min(lowlink.get(quadro.no) ?? 0, indice.get(vizinho) ?? 0)
|
|
14685
|
+
);
|
|
14686
|
+
}
|
|
14687
|
+
continue;
|
|
14688
|
+
}
|
|
14689
|
+
quadros.pop();
|
|
14690
|
+
const pai = quadros[quadros.length - 1];
|
|
14691
|
+
if (pai !== void 0) {
|
|
14692
|
+
lowlink.set(
|
|
14693
|
+
pai.no,
|
|
14694
|
+
Math.min(lowlink.get(pai.no) ?? 0, lowlink.get(quadro.no) ?? 0)
|
|
14695
|
+
);
|
|
14696
|
+
}
|
|
14697
|
+
if (lowlink.get(quadro.no) === indice.get(quadro.no)) {
|
|
14698
|
+
let tamanho = 0;
|
|
14699
|
+
let atual;
|
|
14700
|
+
do {
|
|
14701
|
+
atual = pilha.pop();
|
|
14702
|
+
if (atual === void 0) break;
|
|
14703
|
+
naPilha.delete(atual);
|
|
14704
|
+
tamanho += 1;
|
|
14705
|
+
} while (atual !== quadro.no);
|
|
14706
|
+
if (tamanho > 1) ciclos += 1;
|
|
14707
|
+
}
|
|
14708
|
+
}
|
|
14709
|
+
}
|
|
14710
|
+
return ciclos;
|
|
14711
|
+
}
|
|
14712
|
+
function graphMetrics(graph) {
|
|
14713
|
+
return {
|
|
14714
|
+
nodes: graph.nodes.length,
|
|
14715
|
+
edges: graph.edges.length,
|
|
14716
|
+
routes: hintsFromGraph(graph).apiRoutes.length,
|
|
14717
|
+
packages: graph.packages.length,
|
|
14718
|
+
cycles: importCycles(graph)
|
|
14719
|
+
};
|
|
14720
|
+
}
|
|
14589
14721
|
|
|
14590
|
-
// ../../packages/graph/src/
|
|
14591
|
-
var
|
|
14592
|
-
|
|
14593
|
-
|
|
14722
|
+
// ../../packages/graph/src/identifiers.ts
|
|
14723
|
+
var FROM_CALL = /([A-Za-z_$][\w$]*)\s*\.\s*from\(\s*["'`]([A-Za-z0-9._-]{1,63})["'`]\s*\)/g;
|
|
14724
|
+
var NOT_A_DATABASE = /* @__PURE__ */ new Set([
|
|
14725
|
+
"array",
|
|
14726
|
+
"buffer",
|
|
14727
|
+
"object",
|
|
14728
|
+
"string",
|
|
14729
|
+
"number",
|
|
14730
|
+
"date",
|
|
14731
|
+
"map",
|
|
14732
|
+
"set",
|
|
14733
|
+
"promise",
|
|
14734
|
+
"uint8array",
|
|
14735
|
+
"int8array",
|
|
14736
|
+
"float32array",
|
|
14737
|
+
"float64array",
|
|
14738
|
+
"blob",
|
|
14739
|
+
"json",
|
|
14740
|
+
"bigint",
|
|
14741
|
+
"proxy",
|
|
14742
|
+
"reflect"
|
|
14743
|
+
]);
|
|
14744
|
+
var STORAGE_URL = /\/storage\/v1\/object\/(?:public|sign)\/([A-Za-z0-9._-]{1,63})\//g;
|
|
14745
|
+
var COLLECTION_DOT = /\.\s*collection\(\s*["'`]([A-Za-z][A-Za-z0-9_-]{0,62})["'`]\s*\)/g;
|
|
14746
|
+
var COLLECTION_MODULAR = /\bcollection\(\s*[^,)"'`]+,\s*["'`]([A-Za-z][A-Za-z0-9_-]{0,62})["'`]/g;
|
|
14747
|
+
var VALID_TABLE = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
14748
|
+
var VALID_BUCKET = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/;
|
|
14749
|
+
var SECRETISH = /(sk_live_|sk_test_|rk_live_|pk_live_|pk_test_|sb_secret_|sb_publishable_|eyJ[A-Za-z0-9_-]{10,}|gsk_|sk-ant-|sk-proj-|sk-or-v1-|AIza)/i;
|
|
14750
|
+
function capture(content, pattern, group = 1) {
|
|
14751
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
14752
|
+
const out = [];
|
|
14753
|
+
let m;
|
|
14754
|
+
while ((m = re.exec(content)) !== null) {
|
|
14755
|
+
const value = m[group];
|
|
14756
|
+
if (value !== void 0) out.push(value);
|
|
14757
|
+
if (m[0].length === 0) re.lastIndex += 1;
|
|
14758
|
+
}
|
|
14759
|
+
return out;
|
|
14760
|
+
}
|
|
14761
|
+
function identifiersFromCode(content) {
|
|
14762
|
+
const tables = [];
|
|
14763
|
+
const buckets = [];
|
|
14764
|
+
const re = new RegExp(FROM_CALL.source, FROM_CALL.flags);
|
|
14765
|
+
let m;
|
|
14766
|
+
while ((m = re.exec(content)) !== null) {
|
|
14767
|
+
const receiver = m[1];
|
|
14768
|
+
const name = m[2];
|
|
14769
|
+
if (receiver === void 0 || name === void 0) continue;
|
|
14770
|
+
if (NOT_A_DATABASE.has(receiver.toLowerCase())) continue;
|
|
14771
|
+
if (receiver.toLowerCase() === "storage") {
|
|
14772
|
+
if (VALID_BUCKET.test(name)) buckets.push(name);
|
|
14773
|
+
continue;
|
|
14774
|
+
}
|
|
14775
|
+
if (VALID_TABLE.test(name)) tables.push(name);
|
|
14776
|
+
}
|
|
14777
|
+
for (const name of capture(content, STORAGE_URL)) {
|
|
14778
|
+
if (VALID_BUCKET.test(name)) buckets.push(name);
|
|
14779
|
+
}
|
|
14780
|
+
const collections = [
|
|
14781
|
+
...capture(content, COLLECTION_DOT),
|
|
14782
|
+
...capture(content, COLLECTION_MODULAR)
|
|
14783
|
+
].filter((name) => VALID_TABLE.test(name));
|
|
14784
|
+
return {
|
|
14785
|
+
tables: tables.filter((n) => !SECRETISH.test(n)),
|
|
14786
|
+
buckets: buckets.filter((n) => !SECRETISH.test(n)),
|
|
14787
|
+
collections: collections.filter((n) => !SECRETISH.test(n))
|
|
14788
|
+
};
|
|
14789
|
+
}
|
|
14790
|
+
function mergeIdentifiers(parts, cap) {
|
|
14791
|
+
const dedup = (values, max) => {
|
|
14792
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14793
|
+
const out = [];
|
|
14794
|
+
for (const value of values) {
|
|
14795
|
+
const key = value.toLowerCase();
|
|
14796
|
+
if (seen.has(key)) continue;
|
|
14797
|
+
seen.add(key);
|
|
14798
|
+
out.push(value);
|
|
14799
|
+
if (out.length >= max) break;
|
|
14800
|
+
}
|
|
14801
|
+
return out;
|
|
14802
|
+
};
|
|
14803
|
+
return {
|
|
14804
|
+
tables: dedup(
|
|
14805
|
+
parts.flatMap((p) => p.tables),
|
|
14806
|
+
cap.tables
|
|
14807
|
+
),
|
|
14808
|
+
buckets: dedup(
|
|
14809
|
+
parts.flatMap((p) => p.buckets),
|
|
14810
|
+
cap.buckets
|
|
14811
|
+
),
|
|
14812
|
+
collections: dedup(
|
|
14813
|
+
parts.flatMap((p) => p.collections),
|
|
14814
|
+
cap.collections
|
|
14815
|
+
)
|
|
14816
|
+
};
|
|
14817
|
+
}
|
|
14818
|
+
function isValidDataName(raw, kind) {
|
|
14819
|
+
if (typeof raw !== "string") return null;
|
|
14820
|
+
const cleaned = raw.trim();
|
|
14821
|
+
if (cleaned.length === 0) return null;
|
|
14822
|
+
const pattern = kind === "bucket" ? VALID_BUCKET : VALID_TABLE;
|
|
14823
|
+
if (!pattern.test(cleaned)) return null;
|
|
14824
|
+
if (SECRETISH.test(cleaned)) return null;
|
|
14825
|
+
return cleaned;
|
|
14826
|
+
}
|
|
14827
|
+
|
|
14828
|
+
// ../../packages/graph/src/map.ts
|
|
14829
|
+
var KIND_LABEL = {
|
|
14830
|
+
browser: "browser",
|
|
14831
|
+
app: "application",
|
|
14594
14832
|
api: "API",
|
|
14595
|
-
db: "
|
|
14596
|
-
storage: "
|
|
14597
|
-
auth: "
|
|
14598
|
-
ai: "
|
|
14599
|
-
payments: "
|
|
14600
|
-
secrets: "
|
|
14601
|
-
};
|
|
14602
|
-
var
|
|
14833
|
+
db: "database",
|
|
14834
|
+
storage: "files",
|
|
14835
|
+
auth: "authentication",
|
|
14836
|
+
ai: "AI",
|
|
14837
|
+
payments: "payments",
|
|
14838
|
+
secrets: "security"
|
|
14839
|
+
};
|
|
14840
|
+
var RULES = [
|
|
14603
14841
|
{
|
|
14604
14842
|
kind: "api",
|
|
14605
|
-
|
|
14843
|
+
test: /app\/.+\/route\.(t|j)sx?\b|pages\/api\//i
|
|
14606
14844
|
},
|
|
14607
|
-
{ kind: "payments",
|
|
14608
|
-
{ kind: "auth",
|
|
14609
|
-
{ kind: "ai",
|
|
14610
|
-
{ kind: "storage",
|
|
14845
|
+
{ kind: "payments", test: /\b(stripe|pagarme|mercadopago|paypal|adyen)\b/i },
|
|
14846
|
+
{ kind: "auth", test: /\b(clerk|next-auth|auth0|lucia|kinde|supabase\/auth)\b/i },
|
|
14847
|
+
{ kind: "ai", test: /\b(openai|anthropic|groq|ollama|\bllm\b|langchain)\b/i },
|
|
14848
|
+
{ kind: "storage", test: /\b(storage|s3|r2|bucket|firebase\/storage)\b/i },
|
|
14611
14849
|
{
|
|
14612
14850
|
kind: "db",
|
|
14613
|
-
|
|
14851
|
+
test: /\b(supabase|prisma|drizzle|postgres|mongodb|firebase|firestore|kysely)\b/i
|
|
14614
14852
|
},
|
|
14615
|
-
{ kind: "secrets",
|
|
14616
|
-
{ kind: "browser",
|
|
14853
|
+
{ kind: "secrets", test: /\b(secret|credentials|vault)\b/i },
|
|
14854
|
+
{ kind: "browser", test: /\b(middleware\.(t|j)s|proxy\.(t|j)s)\b/i }
|
|
14617
14855
|
];
|
|
14618
|
-
function
|
|
14619
|
-
const
|
|
14620
|
-
for (const
|
|
14621
|
-
if (
|
|
14856
|
+
function mapKind(signal) {
|
|
14857
|
+
const text = `${signal.label} ${signal.files.join(" ")}`;
|
|
14858
|
+
for (const rule of RULES) {
|
|
14859
|
+
if (rule.test.test(text)) return rule.kind;
|
|
14622
14860
|
}
|
|
14623
14861
|
return "app";
|
|
14624
14862
|
}
|
|
14625
14863
|
|
|
14626
|
-
// ../../packages/graph/src/
|
|
14627
|
-
var
|
|
14628
|
-
var
|
|
14629
|
-
{ id: "next",
|
|
14630
|
-
{ id: "react",
|
|
14631
|
-
{ id: "vue",
|
|
14632
|
-
{ id: "svelte",
|
|
14633
|
-
{ id: "angular",
|
|
14634
|
-
{ id: "vite",
|
|
14635
|
-
{ id: "astro",
|
|
14636
|
-
{ id: "stripe",
|
|
14637
|
-
{ id: "supabase",
|
|
14638
|
-
{ id: "firebase",
|
|
14639
|
-
{ id: "clerk",
|
|
14640
|
-
{ id: "next-auth",
|
|
14641
|
-
{ id: "prisma",
|
|
14642
|
-
{ id: "drizzle",
|
|
14643
|
-
{ id: "kysely",
|
|
14644
|
-
{ id: "postgres",
|
|
14645
|
-
{ id: "mongodb",
|
|
14646
|
-
{ id: "styled-components",
|
|
14647
|
-
{ id: "emotion",
|
|
14648
|
-
{ id: "tailwind",
|
|
14649
|
-
{ id: "openai",
|
|
14650
|
-
{ id: "anthropic",
|
|
14651
|
-
{ id: "langchain",
|
|
14652
|
-
{ id: "trpc",
|
|
14653
|
-
{ id: "hono",
|
|
14654
|
-
{ id: "express",
|
|
14655
|
-
{ id: "fastify",
|
|
14656
|
-
{ id: "graphql",
|
|
14657
|
-
{ id: "redis",
|
|
14658
|
-
{ id: "resend",
|
|
14659
|
-
{ id: "auth0",
|
|
14660
|
-
{ id: "lucia",
|
|
14661
|
-
{ id: "kinde",
|
|
14864
|
+
// ../../packages/graph/src/packages.ts
|
|
14865
|
+
var SECRETISH2 = /(sk_live_|sk_test_|rk_live_|pk_live_|pk_test_|sb_secret_|eyJ[A-Za-z0-9_-]{20,}|gsk_[A-Za-z0-9]|sk-ant-|sk-proj-|sk-or-v1-)/i;
|
|
14866
|
+
var RULES2 = [
|
|
14867
|
+
{ id: "next", test: /^next(?:\/|$)/ },
|
|
14868
|
+
{ id: "react", test: /^react(?:-dom)?(?:\/|$)/ },
|
|
14869
|
+
{ id: "vue", test: /^vue(?:\/|$)/ },
|
|
14870
|
+
{ id: "svelte", test: /^svelte(?:\/|$)/ },
|
|
14871
|
+
{ id: "angular", test: /^@angular\// },
|
|
14872
|
+
{ id: "vite", test: /^vite(?:\/|$)/ },
|
|
14873
|
+
{ id: "astro", test: /^astro(?:\/|$)/ },
|
|
14874
|
+
{ id: "stripe", test: /^(?:stripe|@stripe\/)/ },
|
|
14875
|
+
{ id: "supabase", test: /^(?:@supabase\/|@supabase$)/ },
|
|
14876
|
+
{ id: "firebase", test: /^(?:firebase|@firebase\/)/ },
|
|
14877
|
+
{ id: "clerk", test: /^@clerk\// },
|
|
14878
|
+
{ id: "next-auth", test: /^(?:next-auth|@auth\/)/ },
|
|
14879
|
+
{ id: "prisma", test: /^(?:@prisma\/client|prisma)(?:\/|$)/ },
|
|
14880
|
+
{ id: "drizzle", test: /^drizzle-orm(?:\/|$)/ },
|
|
14881
|
+
{ id: "kysely", test: /^kysely(?:\/|$)/ },
|
|
14882
|
+
{ id: "postgres", test: /^(?:postgres|pg|@neondatabase\/)/ },
|
|
14883
|
+
{ id: "mongodb", test: /^(?:mongodb|mongoose)(?:\/|$)/ },
|
|
14884
|
+
{ id: "styled-components", test: /^styled-components(?:\/|$)/ },
|
|
14885
|
+
{ id: "emotion", test: /^@emotion\// },
|
|
14886
|
+
{ id: "tailwind", test: /^tailwindcss(?:\/|$)/ },
|
|
14887
|
+
{ id: "openai", test: /^openai(?:\/|$)/ },
|
|
14888
|
+
{ id: "anthropic", test: /^@anthropic-ai\// },
|
|
14889
|
+
{ id: "langchain", test: /^langchain(?:\/|$)/ },
|
|
14890
|
+
{ id: "trpc", test: /^@trpc\// },
|
|
14891
|
+
{ id: "hono", test: /^hono(?:\/|$)/ },
|
|
14892
|
+
{ id: "express", test: /^express(?:\/|$)/ },
|
|
14893
|
+
{ id: "fastify", test: /^fastify(?:\/|$)/ },
|
|
14894
|
+
{ id: "graphql", test: /^(?:graphql|@apollo\/)/ },
|
|
14895
|
+
{ id: "redis", test: /^(?:redis|ioredis)(?:\/|$)/ },
|
|
14896
|
+
{ id: "resend", test: /^resend(?:\/|$)/ },
|
|
14897
|
+
{ id: "auth0", test: /^(?:auth0|@auth0\/)/ },
|
|
14898
|
+
{ id: "lucia", test: /^lucia(?:\/|$)/ },
|
|
14899
|
+
{ id: "kinde", test: /^@kinde-oss\// }
|
|
14662
14900
|
];
|
|
14663
|
-
function
|
|
14664
|
-
const
|
|
14665
|
-
if (
|
|
14666
|
-
|
|
14667
|
-
|
|
14668
|
-
|
|
14901
|
+
function packageId(spec) {
|
|
14902
|
+
const cleaned = spec.trim().replace(/^npm:/, "");
|
|
14903
|
+
if (cleaned.length === 0 || cleaned.startsWith(".") || cleaned.startsWith("/"))
|
|
14904
|
+
return null;
|
|
14905
|
+
if (cleaned.startsWith("node:") || cleaned.startsWith("#")) return null;
|
|
14906
|
+
for (const rule of RULES2) {
|
|
14907
|
+
if (rule.test.test(cleaned)) return rule.id;
|
|
14669
14908
|
}
|
|
14670
14909
|
return null;
|
|
14671
14910
|
}
|
|
14672
|
-
function
|
|
14673
|
-
const
|
|
14674
|
-
if (
|
|
14675
|
-
if (
|
|
14676
|
-
if (!/^[@a-z0-9][a-z0-9._+/-]{0,60}$/i.test(
|
|
14677
|
-
return
|
|
14911
|
+
function sanitizePackage(raw) {
|
|
14912
|
+
const cleaned = raw.trim().slice(0, MAX_LABEL);
|
|
14913
|
+
if (cleaned.length === 0) return null;
|
|
14914
|
+
if (SECRETISH2.test(cleaned)) return null;
|
|
14915
|
+
if (!/^[@a-z0-9][a-z0-9._+/-]{0,60}$/i.test(cleaned)) return null;
|
|
14916
|
+
return cleaned;
|
|
14678
14917
|
}
|
|
14679
|
-
function
|
|
14918
|
+
function uniquePackages(values) {
|
|
14680
14919
|
const seen = /* @__PURE__ */ new Set();
|
|
14681
14920
|
const out = [];
|
|
14682
14921
|
for (const value of values) {
|
|
14683
|
-
if (out.length >=
|
|
14684
|
-
const
|
|
14685
|
-
if (
|
|
14686
|
-
seen.add(
|
|
14687
|
-
out.push(
|
|
14922
|
+
if (out.length >= MAX_PACKAGES) break;
|
|
14923
|
+
const cleaned = sanitizePackage(value);
|
|
14924
|
+
if (cleaned === null || seen.has(cleaned)) continue;
|
|
14925
|
+
seen.add(cleaned);
|
|
14926
|
+
out.push(cleaned);
|
|
14688
14927
|
}
|
|
14689
14928
|
return out;
|
|
14690
14929
|
}
|
|
14691
14930
|
|
|
14692
14931
|
// ../../packages/graph/src/sanitize.ts
|
|
14693
|
-
var
|
|
14932
|
+
var BODY_FIELDS = /* @__PURE__ */ new Set([
|
|
14694
14933
|
"content",
|
|
14695
14934
|
"snippet",
|
|
14696
14935
|
"code",
|
|
@@ -14702,34 +14941,43 @@ var CAMPOS_CORPO = /* @__PURE__ */ new Set([
|
|
|
14702
14941
|
"raw",
|
|
14703
14942
|
"bytes"
|
|
14704
14943
|
]);
|
|
14705
|
-
var
|
|
14706
|
-
var
|
|
14707
|
-
var
|
|
14708
|
-
var
|
|
14709
|
-
function
|
|
14944
|
+
var SECRET_EXTENSIONS = /\.(env|pem|key|p12|pfx|crt|cer|p8|keystore)(?:\.|$)/i;
|
|
14945
|
+
var FORBIDDEN_SEGMENT = /(?:^|\/)(?:\.env(?:\..+)?|id_rsa|id_ed25519|credentials|secrets?)(?:\/|$)/i;
|
|
14946
|
+
var SECRETISH3 = /(sk_live_|sk_test_|rk_live_|pk_live_|pk_test_|sb_secret_|sb_publishable_|eyJ[A-Za-z0-9_-]{20,}|gsk_[A-Za-z0-9]|sk-ant-|sk-proj-|sk-or-v1-)/i;
|
|
14947
|
+
var MAX_FILE_LABEL = MAX_PATH;
|
|
14948
|
+
function isRecord(value) {
|
|
14710
14949
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14711
14950
|
}
|
|
14712
|
-
function
|
|
14951
|
+
function asString(value) {
|
|
14713
14952
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
14714
14953
|
}
|
|
14715
|
-
function
|
|
14716
|
-
const
|
|
14717
|
-
if (
|
|
14718
|
-
if (
|
|
14719
|
-
if (
|
|
14720
|
-
if (
|
|
14721
|
-
if (
|
|
14722
|
-
if (/(?:^|\/)node_modules\//.test(
|
|
14723
|
-
if (/(?:^|\/)\.git\//.test(
|
|
14724
|
-
return
|
|
14725
|
-
}
|
|
14726
|
-
function
|
|
14727
|
-
const
|
|
14728
|
-
if (
|
|
14729
|
-
if (
|
|
14730
|
-
return
|
|
14731
|
-
|
|
14732
|
-
|
|
14954
|
+
function sanitizePath(raw) {
|
|
14955
|
+
const trimmed = raw.trim().replaceAll("\\", "/");
|
|
14956
|
+
if (trimmed.length === 0 || trimmed.length > MAX_FILE_LABEL) return null;
|
|
14957
|
+
if (trimmed.includes("\0")) return null;
|
|
14958
|
+
if (trimmed.startsWith("/") || /^[a-zA-Z]:/.test(trimmed)) return null;
|
|
14959
|
+
if (trimmed.split("/").some((p) => p === ".." || p === ".")) return null;
|
|
14960
|
+
if (SECRET_EXTENSIONS.test(trimmed) || FORBIDDEN_SEGMENT.test(trimmed)) return null;
|
|
14961
|
+
if (/(?:^|\/)node_modules\//.test(trimmed)) return null;
|
|
14962
|
+
if (/(?:^|\/)\.git\//.test(trimmed)) return null;
|
|
14963
|
+
return trimmed.slice(0, MAX_PATH);
|
|
14964
|
+
}
|
|
14965
|
+
function redactLabel(raw) {
|
|
14966
|
+
const cleaned = raw.trim().slice(0, MAX_LABEL);
|
|
14967
|
+
if (cleaned.length === 0) return "node";
|
|
14968
|
+
if (SECRETISH3.test(cleaned)) return "[redacted]";
|
|
14969
|
+
if (looksLikePath(cleaned) && sanitizePath(cleaned) === null) return "[redacted]";
|
|
14970
|
+
return cleaned;
|
|
14971
|
+
}
|
|
14972
|
+
function looksLikePath(value) {
|
|
14973
|
+
return /[\\/]/.test(value) || /^[a-zA-Z]:/.test(value);
|
|
14974
|
+
}
|
|
14975
|
+
function opaque(value) {
|
|
14976
|
+
let h = 5381;
|
|
14977
|
+
for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) | 0;
|
|
14978
|
+
return `p${(h >>> 0).toString(36)}`;
|
|
14979
|
+
}
|
|
14980
|
+
function uniqueCapped2(values, cap) {
|
|
14733
14981
|
const seen = /* @__PURE__ */ new Set();
|
|
14734
14982
|
const out = [];
|
|
14735
14983
|
for (const value of values) {
|
|
@@ -14740,252 +14988,417 @@ function uniqueCapped(values, cap) {
|
|
|
14740
14988
|
}
|
|
14741
14989
|
return out;
|
|
14742
14990
|
}
|
|
14743
|
-
function
|
|
14744
|
-
const
|
|
14745
|
-
if (typeof value === "string")
|
|
14991
|
+
function filesFrom(value) {
|
|
14992
|
+
const raw = [];
|
|
14993
|
+
if (typeof value === "string") raw.push(value);
|
|
14746
14994
|
if (Array.isArray(value)) {
|
|
14747
14995
|
for (const item of value) {
|
|
14748
|
-
if (typeof item === "string")
|
|
14996
|
+
if (typeof item === "string") raw.push(item);
|
|
14749
14997
|
}
|
|
14750
14998
|
}
|
|
14751
|
-
const
|
|
14752
|
-
for (const
|
|
14753
|
-
const
|
|
14754
|
-
if (
|
|
14999
|
+
const cleaned = [];
|
|
15000
|
+
for (const item of raw) {
|
|
15001
|
+
const path = sanitizePath(item);
|
|
15002
|
+
if (path !== null) cleaned.push(path);
|
|
14755
15003
|
}
|
|
14756
|
-
return
|
|
15004
|
+
return uniqueCapped2(cleaned, MAX_FILES_PER_NODE);
|
|
14757
15005
|
}
|
|
14758
|
-
function
|
|
14759
|
-
const
|
|
14760
|
-
|
|
15006
|
+
function idFrom(value, fallback) {
|
|
15007
|
+
const raw = (asString(value) ?? fallback).trim().slice(0, MAX_ID);
|
|
15008
|
+
const safe = raw.split("::").map(
|
|
15009
|
+
(part) => looksLikePath(part) && sanitizePath(part) === null ? opaque(part) : part
|
|
15010
|
+
).join("::");
|
|
15011
|
+
return safe.replace(/[^\w.:/-]/g, "_") || fallback;
|
|
14761
15012
|
}
|
|
14762
|
-
function
|
|
14763
|
-
const
|
|
14764
|
-
if (
|
|
14765
|
-
if (
|
|
15013
|
+
function edgeType(value) {
|
|
15014
|
+
const raw = (asString(value) ?? "contains").toLowerCase();
|
|
15015
|
+
if (raw.includes("import")) return "imports";
|
|
15016
|
+
if (raw.includes("call")) return "calls";
|
|
14766
15017
|
return "contains";
|
|
14767
15018
|
}
|
|
14768
|
-
function
|
|
14769
|
-
if (Array.isArray(value)) return value.map(
|
|
14770
|
-
if (!
|
|
15019
|
+
function stripBody(value) {
|
|
15020
|
+
if (Array.isArray(value)) return value.map(stripBody);
|
|
15021
|
+
if (!isRecord(value)) return value;
|
|
14771
15022
|
const out = {};
|
|
14772
|
-
for (const [
|
|
14773
|
-
if (
|
|
14774
|
-
if (
|
|
15023
|
+
for (const [key, item] of Object.entries(value)) {
|
|
15024
|
+
if (BODY_FIELDS.has(key)) continue;
|
|
15025
|
+
if (key === "source" && typeof item === "string" && (item.includes("\n") || item.length > MAX_ID)) {
|
|
14775
15026
|
continue;
|
|
14776
15027
|
}
|
|
14777
|
-
out[
|
|
15028
|
+
out[key] = stripBody(item);
|
|
14778
15029
|
}
|
|
14779
15030
|
return out;
|
|
14780
15031
|
}
|
|
14781
|
-
function
|
|
15032
|
+
function firstExport(value) {
|
|
14782
15033
|
if (typeof value === "string" && value.length > 0) {
|
|
14783
|
-
const
|
|
14784
|
-
return
|
|
15034
|
+
const first = value.split(/[\s,]+/)[0];
|
|
15035
|
+
return first !== void 0 && first.length > 0 ? first : null;
|
|
14785
15036
|
}
|
|
14786
15037
|
if (!Array.isArray(value)) return null;
|
|
14787
15038
|
for (const item of value) {
|
|
14788
15039
|
if (typeof item === "string" && item.length > 0) return item;
|
|
14789
|
-
if (
|
|
14790
|
-
const
|
|
14791
|
-
if (
|
|
15040
|
+
if (isRecord(item)) {
|
|
15041
|
+
const name = asString(item["name"]) ?? asString(item["identifier"]);
|
|
15042
|
+
if (name !== null) return name;
|
|
14792
15043
|
}
|
|
14793
15044
|
}
|
|
14794
15045
|
return null;
|
|
14795
15046
|
}
|
|
14796
|
-
function
|
|
14797
|
-
const
|
|
14798
|
-
const
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
const arquivos = arquivosDe(
|
|
14805
|
-
item["arquivos"] ?? item["files"] ?? item["source_file"] ?? item["sourceFile"] ?? item["file"] ?? item["path"]
|
|
15047
|
+
function nodesFromDump(dump) {
|
|
15048
|
+
const raw = dump["nodes"] ?? dump["nos"];
|
|
15049
|
+
const list = Array.isArray(raw) ? raw : isRecord(raw) ? Object.entries(raw).map(([id, n]) => isRecord(n) ? { id, ...n } : { id }) : [];
|
|
15050
|
+
const nodes = [];
|
|
15051
|
+
for (const [i, item] of list.entries()) {
|
|
15052
|
+
if (!isRecord(item)) continue;
|
|
15053
|
+
const files = filesFrom(
|
|
15054
|
+
item["files"] ?? item["files"] ?? item["source_file"] ?? item["sourceFile"] ?? item["file"] ?? item["path"]
|
|
14806
15055
|
);
|
|
14807
|
-
const
|
|
14808
|
-
|
|
15056
|
+
const label = redactLabel(
|
|
15057
|
+
asString(item["label"]) ?? asString(item["label"]) ?? asString(item["name"]) ?? asString(item["identifier"]) ?? asString(item["symbol"]) ?? firstExport(item["exports"] ?? item["identificadores"]) ?? files[0] ?? `node-${String(i)}`
|
|
14809
15058
|
);
|
|
14810
|
-
const
|
|
14811
|
-
const kind =
|
|
14812
|
-
|
|
14813
|
-
id:
|
|
15059
|
+
const kindRaw = asString(item["kind"]) ?? asString(item["nodeKind"]);
|
|
15060
|
+
const kind = kindRaw !== null && NODE_KINDS.includes(kindRaw) ? kindRaw : mapKind({ label, files });
|
|
15061
|
+
nodes.push({
|
|
15062
|
+
id: idFrom(item["id"], `n${String(i)}`),
|
|
14814
15063
|
kind,
|
|
14815
|
-
|
|
14816
|
-
|
|
15064
|
+
label,
|
|
15065
|
+
files
|
|
14817
15066
|
});
|
|
14818
|
-
if (
|
|
15067
|
+
if (nodes.length >= MAX_NODES) break;
|
|
14819
15068
|
}
|
|
14820
|
-
return
|
|
15069
|
+
return nodes;
|
|
14821
15070
|
}
|
|
14822
|
-
function
|
|
14823
|
-
const
|
|
14824
|
-
const
|
|
15071
|
+
function edgesFromDump(dump, ids) {
|
|
15072
|
+
const raw = dump["edges"] ?? dump["edges"];
|
|
15073
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
14825
15074
|
const out = [];
|
|
14826
15075
|
const seen = /* @__PURE__ */ new Set();
|
|
14827
|
-
for (const item of
|
|
14828
|
-
if (!
|
|
14829
|
-
const
|
|
14830
|
-
const
|
|
14831
|
-
if (
|
|
14832
|
-
if (!ids.has(
|
|
14833
|
-
const
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
15076
|
+
for (const item of list) {
|
|
15077
|
+
if (!isRecord(item)) continue;
|
|
15078
|
+
const from = idFrom(item["from"] ?? item["de"] ?? item["source"], "");
|
|
15079
|
+
const to = idFrom(item["to"] ?? item["para"] ?? item["target"], "");
|
|
15080
|
+
if (from.length === 0 || to.length === 0) continue;
|
|
15081
|
+
if (!ids.has(from) || !ids.has(to)) continue;
|
|
15082
|
+
const type = edgeType(
|
|
15083
|
+
item["type"] ?? item["tipo"] ?? item["relation"] ?? item["kind"]
|
|
15084
|
+
);
|
|
15085
|
+
const key = `${from}>${to}:${type}`;
|
|
15086
|
+
if (seen.has(key)) continue;
|
|
15087
|
+
seen.add(key);
|
|
15088
|
+
out.push({ from, to, type });
|
|
15089
|
+
if (out.length >= MAX_EDGES) break;
|
|
14839
15090
|
}
|
|
14840
15091
|
return out;
|
|
14841
15092
|
}
|
|
14842
|
-
function
|
|
14843
|
-
const
|
|
14844
|
-
if (Array.isArray(
|
|
15093
|
+
function communitiesFromDump(dump, nodes) {
|
|
15094
|
+
const alreadyShort = dump["communities"] ?? dump["communities"];
|
|
15095
|
+
if (Array.isArray(alreadyShort)) {
|
|
14845
15096
|
const out2 = [];
|
|
14846
15097
|
const seen = /* @__PURE__ */ new Set();
|
|
14847
|
-
for (const item of
|
|
14848
|
-
if (!
|
|
14849
|
-
const
|
|
14850
|
-
if (
|
|
15098
|
+
for (const item of alreadyShort) {
|
|
15099
|
+
if (!isRecord(item)) continue;
|
|
15100
|
+
const kindRaw = asString(item["nodeKind"]) ?? asString(item["kind"]);
|
|
15101
|
+
if (kindRaw === null || !NODE_KINDS.includes(kindRaw)) {
|
|
14851
15102
|
continue;
|
|
14852
15103
|
}
|
|
14853
|
-
const nodeKind =
|
|
15104
|
+
const nodeKind = kindRaw;
|
|
14854
15105
|
if (seen.has(nodeKind)) {
|
|
14855
|
-
const
|
|
14856
|
-
if (
|
|
14857
|
-
const extra =
|
|
14858
|
-
const merged =
|
|
14859
|
-
out2.splice(out2.indexOf(
|
|
15106
|
+
const current = out2.find((c) => c.nodeKind === nodeKind);
|
|
15107
|
+
if (current === void 0) continue;
|
|
15108
|
+
const extra = filesFrom(item["files"] ?? item["files"]);
|
|
15109
|
+
const merged = uniqueCapped2([...current.files, ...extra], MAX_COMMUNITY_FILES);
|
|
15110
|
+
out2.splice(out2.indexOf(current), 1, {
|
|
14860
15111
|
nodeKind,
|
|
14861
|
-
|
|
14862
|
-
|
|
15112
|
+
label: current.label,
|
|
15113
|
+
files: merged
|
|
14863
15114
|
});
|
|
14864
15115
|
continue;
|
|
14865
15116
|
}
|
|
14866
15117
|
seen.add(nodeKind);
|
|
14867
15118
|
out2.push({
|
|
14868
15119
|
nodeKind,
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
15120
|
+
label: KIND_LABEL[nodeKind],
|
|
15121
|
+
files: uniqueCapped2(
|
|
15122
|
+
filesFrom(item["files"] ?? item["files"]),
|
|
15123
|
+
MAX_COMMUNITY_FILES
|
|
14873
15124
|
)
|
|
14874
15125
|
});
|
|
14875
|
-
if (out2.length >=
|
|
15126
|
+
if (out2.length >= MAX_COMMUNITIES) break;
|
|
14876
15127
|
}
|
|
14877
15128
|
return out2;
|
|
14878
15129
|
}
|
|
14879
|
-
const
|
|
14880
|
-
for (const
|
|
14881
|
-
const
|
|
14882
|
-
|
|
14883
|
-
|
|
15130
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
15131
|
+
for (const node of nodes) {
|
|
15132
|
+
const current = byKind.get(node.kind) ?? { label: node.label, files: [] };
|
|
15133
|
+
current.files.push(...node.files);
|
|
15134
|
+
byKind.set(node.kind, current);
|
|
14884
15135
|
}
|
|
14885
15136
|
const out = [];
|
|
14886
15137
|
for (const kind of NODE_KINDS) {
|
|
14887
|
-
const
|
|
14888
|
-
if (
|
|
14889
|
-
const
|
|
14890
|
-
if (
|
|
14891
|
-
out.push({ nodeKind: kind,
|
|
14892
|
-
if (out.length >=
|
|
15138
|
+
const group = byKind.get(kind);
|
|
15139
|
+
if (group === void 0) continue;
|
|
15140
|
+
const files = uniqueCapped2(group.files, MAX_COMMUNITY_FILES);
|
|
15141
|
+
if (files.length === 0 && group.label.length === 0) continue;
|
|
15142
|
+
out.push({ nodeKind: kind, label: KIND_LABEL[kind], files });
|
|
15143
|
+
if (out.length >= MAX_COMMUNITIES) break;
|
|
14893
15144
|
}
|
|
14894
15145
|
return out;
|
|
14895
15146
|
}
|
|
14896
|
-
function
|
|
14897
|
-
if (
|
|
15147
|
+
function sanitizeGraph(input) {
|
|
15148
|
+
if (input === null || input === void 0) return { ok: false, reason: "invalid" };
|
|
14898
15149
|
let json2;
|
|
14899
15150
|
try {
|
|
14900
|
-
json2 = JSON.stringify(
|
|
15151
|
+
json2 = JSON.stringify(input);
|
|
14901
15152
|
} catch {
|
|
14902
|
-
return { ok: false,
|
|
14903
|
-
}
|
|
14904
|
-
if (json2.length > MAX_BYTES) return { ok: false,
|
|
14905
|
-
const
|
|
14906
|
-
if (!
|
|
14907
|
-
const
|
|
14908
|
-
const ids = new Set(
|
|
14909
|
-
const
|
|
14910
|
-
const
|
|
14911
|
-
const
|
|
14912
|
-
const
|
|
14913
|
-
|
|
14914
|
-
|
|
15153
|
+
return { ok: false, reason: "invalid" };
|
|
15154
|
+
}
|
|
15155
|
+
if (json2.length > MAX_BYTES) return { ok: false, reason: "too-large" };
|
|
15156
|
+
const cleaned = stripBody(input);
|
|
15157
|
+
if (!isRecord(cleaned)) return { ok: false, reason: "invalid" };
|
|
15158
|
+
const nodes = nodesFromDump(cleaned);
|
|
15159
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
15160
|
+
const edges = edgesFromDump(cleaned, ids);
|
|
15161
|
+
const communities = communitiesFromDump(cleaned, nodes);
|
|
15162
|
+
const packages = uniquePackages(packagesFromDump(cleaned, nodes));
|
|
15163
|
+
const data = dataFromDump(cleaned);
|
|
15164
|
+
const hasFile = communities.some((c) => c.files.length > 0) || nodes.some((n) => n.files.length > 0);
|
|
15165
|
+
if (!hasFile) return { ok: false, reason: "empty" };
|
|
15166
|
+
const advanced = communities.length > 0;
|
|
14915
15167
|
return {
|
|
14916
15168
|
ok: true,
|
|
14917
|
-
|
|
14918
|
-
|
|
14919
|
-
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
14923
|
-
|
|
15169
|
+
graph: {
|
|
15170
|
+
version: 1,
|
|
15171
|
+
advanced,
|
|
15172
|
+
nodes,
|
|
15173
|
+
edges,
|
|
15174
|
+
communities,
|
|
15175
|
+
packages,
|
|
15176
|
+
...data === void 0 ? {} : { data }
|
|
15177
|
+
}
|
|
15178
|
+
};
|
|
15179
|
+
}
|
|
15180
|
+
function dataFromDump(dump) {
|
|
15181
|
+
const raw = dump["data"] ?? dump["dados"];
|
|
15182
|
+
if (!isRecord(raw)) return void 0;
|
|
15183
|
+
const clean = (value, kind, cap) => {
|
|
15184
|
+
if (!Array.isArray(value)) return [];
|
|
15185
|
+
const cleaned = [];
|
|
15186
|
+
for (const item of value) {
|
|
15187
|
+
const name = isValidDataName(item, kind);
|
|
15188
|
+
if (name !== null) cleaned.push(name);
|
|
14924
15189
|
}
|
|
15190
|
+
return uniqueCapped2(cleaned, cap);
|
|
15191
|
+
};
|
|
15192
|
+
const data = {
|
|
15193
|
+
tables: clean(raw["tables"] ?? raw["tables"], "table", MAX_HINT_TABLES),
|
|
15194
|
+
buckets: clean(raw["buckets"], "bucket", MAX_HINT_BUCKETS),
|
|
15195
|
+
collections: clean(
|
|
15196
|
+
raw["collections"] ?? raw["collections"],
|
|
15197
|
+
"table",
|
|
15198
|
+
MAX_HINT_COLLECTIONS
|
|
15199
|
+
)
|
|
14925
15200
|
};
|
|
15201
|
+
if (data.tables.length === 0 && data.buckets.length === 0 && data.collections.length === 0) {
|
|
15202
|
+
return void 0;
|
|
15203
|
+
}
|
|
15204
|
+
return data;
|
|
14926
15205
|
}
|
|
14927
|
-
function
|
|
14928
|
-
const
|
|
14929
|
-
const
|
|
14930
|
-
if (Array.isArray(
|
|
14931
|
-
for (const item of
|
|
14932
|
-
if (typeof item === "string")
|
|
15206
|
+
function packagesFromDump(dump, nodes) {
|
|
15207
|
+
const raw = [];
|
|
15208
|
+
const field = dump["packages"] ?? dump["packages"];
|
|
15209
|
+
if (Array.isArray(field)) {
|
|
15210
|
+
for (const item of field) {
|
|
15211
|
+
if (typeof item === "string") raw.push(item);
|
|
14933
15212
|
}
|
|
14934
15213
|
}
|
|
14935
|
-
for (const
|
|
14936
|
-
if (
|
|
15214
|
+
for (const node of nodes) {
|
|
15215
|
+
if (node.id.startsWith("pkg:")) raw.push(node.label);
|
|
14937
15216
|
}
|
|
14938
|
-
return
|
|
15217
|
+
return raw;
|
|
14939
15218
|
}
|
|
14940
15219
|
|
|
14941
|
-
// src/
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
14962
|
-
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
15220
|
+
// ../../packages/schema/src/quality-gate.ts
|
|
15221
|
+
var QUALITY_GATE_POLICY = "2026-code-gate-v1";
|
|
15222
|
+
var GATE_STATUSES = ["passed", "failed", "inconclusive"];
|
|
15223
|
+
var HTTP_SCAN_OUTCOMES = [
|
|
15224
|
+
"ran",
|
|
15225
|
+
"skipped-quota",
|
|
15226
|
+
"skipped-flag",
|
|
15227
|
+
"failed"
|
|
15228
|
+
];
|
|
15229
|
+
var ISSUE_SOURCES = ["code", "http"];
|
|
15230
|
+
var REPROVA = ["fatal", "critical", "high"];
|
|
15231
|
+
function reprova(severity) {
|
|
15232
|
+
return REPROVA.includes(severity);
|
|
15233
|
+
}
|
|
15234
|
+
function contarPorSeveridade(issues) {
|
|
15235
|
+
const counts = Object.fromEntries(SEVERITIES.map((s) => [s, 0]));
|
|
15236
|
+
for (const issue2 of issues) counts[issue2.severity] += 1;
|
|
15237
|
+
return counts;
|
|
15238
|
+
}
|
|
15239
|
+
function analysed(report) {
|
|
15240
|
+
return report.codeFiles > 0 || report.httpScan === "ran";
|
|
15241
|
+
}
|
|
15242
|
+
function statusFromIssues(issues, httpScan, analisou) {
|
|
15243
|
+
if (!analisou) return "inconclusive";
|
|
15244
|
+
if (issues.some((i) => reprova(i.severity))) return "failed";
|
|
15245
|
+
return httpScan === "failed" ? "inconclusive" : "passed";
|
|
15246
|
+
}
|
|
15247
|
+
var GATE_EXIT = {
|
|
15248
|
+
passed: 0,
|
|
15249
|
+
failed: 1,
|
|
15250
|
+
inconclusive: 2,
|
|
15251
|
+
/** Erro operacional: autenticação, transporte, persistência. */
|
|
15252
|
+
operational: 3
|
|
15253
|
+
};
|
|
15254
|
+
function exitCodeFromStatus(status) {
|
|
15255
|
+
return GATE_EXIT[status];
|
|
15256
|
+
}
|
|
15257
|
+
var MAX_ISSUES = 200;
|
|
15258
|
+
var MAX_REASON = 200;
|
|
15259
|
+
var MAX_PATH2 = 180;
|
|
15260
|
+
var issueSchema = external_exports.object({
|
|
15261
|
+
checkId: external_exports.string().min(1).max(80),
|
|
15262
|
+
severity: external_exports.enum(SEVERITIES),
|
|
15263
|
+
path: external_exports.string().min(1).max(MAX_PATH2),
|
|
15264
|
+
reason: external_exports.string().min(1).max(MAX_REASON),
|
|
15265
|
+
source: external_exports.enum(ISSUE_SOURCES)
|
|
15266
|
+
});
|
|
15267
|
+
var metricsSchema = external_exports.object({
|
|
15268
|
+
nodes: external_exports.number().int().nonnegative(),
|
|
15269
|
+
edges: external_exports.number().int().nonnegative(),
|
|
15270
|
+
routes: external_exports.number().int().nonnegative(),
|
|
15271
|
+
packages: external_exports.number().int().nonnegative(),
|
|
15272
|
+
cycles: external_exports.number().int().nonnegative()
|
|
15273
|
+
});
|
|
15274
|
+
var qualityGateSchema = external_exports.object({
|
|
15275
|
+
policy: external_exports.literal(QUALITY_GATE_POLICY),
|
|
15276
|
+
status: external_exports.enum(GATE_STATUSES),
|
|
15277
|
+
httpScan: external_exports.enum(HTTP_SCAN_OUTCOMES),
|
|
15278
|
+
issues: external_exports.array(issueSchema).max(MAX_ISSUES),
|
|
15279
|
+
counts: external_exports.record(external_exports.enum(SEVERITIES), external_exports.number().int().nonnegative()),
|
|
15280
|
+
metrics: metricsSchema.nullable(),
|
|
15281
|
+
codeFiles: external_exports.number().int().nonnegative(),
|
|
15282
|
+
ranAt: external_exports.string().min(1)
|
|
15283
|
+
});
|
|
14976
15284
|
|
|
14977
|
-
// src/
|
|
14978
|
-
|
|
15285
|
+
// ../../packages/schema/src/stages.ts
|
|
15286
|
+
var SCAN_STAGES = [
|
|
15287
|
+
"connecting",
|
|
15288
|
+
"reading-app",
|
|
15289
|
+
"secrets",
|
|
15290
|
+
"data-files",
|
|
15291
|
+
"api-ai",
|
|
15292
|
+
"config",
|
|
15293
|
+
"verdict"
|
|
15294
|
+
];
|
|
15295
|
+
var CHECK_OUTCOMES = ["passed", "found", "skipped", "error"];
|
|
15296
|
+
var SKIP_REASONS = ["requires-verified", "requires-consent"];
|
|
15297
|
+
|
|
15298
|
+
// ../../packages/schema/src/events.ts
|
|
15299
|
+
var VERDICTS = ["sealed", "exposed"];
|
|
15300
|
+
var nodeId = external_exports.enum(NODE_KINDS);
|
|
15301
|
+
var scanEventSchema = external_exports.discriminatedUnion("type", [
|
|
15302
|
+
external_exports.object({
|
|
15303
|
+
type: external_exports.literal("node.discovered"),
|
|
15304
|
+
nodeId,
|
|
15305
|
+
kind: external_exports.enum(NODE_KINDS),
|
|
15306
|
+
label: external_exports.string().min(1),
|
|
15307
|
+
parentId: nodeId.optional()
|
|
15308
|
+
}),
|
|
15309
|
+
external_exports.object({ type: external_exports.literal("node.probing"), nodeId }),
|
|
15310
|
+
external_exports.object({ type: external_exports.literal("node.sealed"), nodeId }),
|
|
15311
|
+
external_exports.object({
|
|
15312
|
+
type: external_exports.literal("finding.found"),
|
|
15313
|
+
checkId: external_exports.string().min(1),
|
|
15314
|
+
severity: external_exports.enum(SEVERITIES),
|
|
15315
|
+
nodeId,
|
|
15316
|
+
proof: external_exports.string().min(1),
|
|
15317
|
+
subject: external_exports.string().optional()
|
|
15318
|
+
}),
|
|
15319
|
+
external_exports.object({
|
|
15320
|
+
type: external_exports.literal("scan.done"),
|
|
15321
|
+
verdict: external_exports.enum(VERDICTS),
|
|
15322
|
+
exposureCount: external_exports.number().int().nonnegative(),
|
|
15323
|
+
exposedNodes: external_exports.number().int().nonnegative(),
|
|
15324
|
+
stack: external_exports.object({
|
|
15325
|
+
language: external_exports.array(external_exports.string()),
|
|
15326
|
+
framework: external_exports.array(external_exports.string()),
|
|
15327
|
+
styling: external_exports.array(external_exports.string())
|
|
15328
|
+
}).optional(),
|
|
15329
|
+
coverage: external_exports.object({
|
|
15330
|
+
bundles: external_exports.number().int().nonnegative(),
|
|
15331
|
+
routes: external_exports.number().int().nonnegative(),
|
|
15332
|
+
tables: external_exports.number().int().nonnegative(),
|
|
15333
|
+
buckets: external_exports.number().int().nonnegative(),
|
|
15334
|
+
withGraph: external_exports.boolean()
|
|
15335
|
+
}).optional()
|
|
15336
|
+
}),
|
|
15337
|
+
external_exports.object({ type: external_exports.literal("scan.failed"), reason: external_exports.string().min(1) }),
|
|
15338
|
+
external_exports.object({
|
|
15339
|
+
type: external_exports.literal("graph.ready"),
|
|
15340
|
+
communities: external_exports.array(
|
|
15341
|
+
external_exports.object({
|
|
15342
|
+
nodeKind: external_exports.enum(NODE_KINDS),
|
|
15343
|
+
label: external_exports.string().min(1),
|
|
15344
|
+
files: external_exports.array(external_exports.string())
|
|
15345
|
+
})
|
|
15346
|
+
),
|
|
15347
|
+
nodes: external_exports.array(
|
|
15348
|
+
external_exports.object({
|
|
15349
|
+
id: external_exports.string().min(1),
|
|
15350
|
+
kind: external_exports.enum(NODE_KINDS),
|
|
15351
|
+
label: external_exports.string().min(1),
|
|
15352
|
+
files: external_exports.array(external_exports.string())
|
|
15353
|
+
})
|
|
15354
|
+
).optional(),
|
|
15355
|
+
edges: external_exports.array(
|
|
15356
|
+
external_exports.object({
|
|
15357
|
+
from: external_exports.string().min(1),
|
|
15358
|
+
to: external_exports.string().min(1),
|
|
15359
|
+
type: external_exports.enum(["imports", "calls", "contains"])
|
|
15360
|
+
})
|
|
15361
|
+
).optional(),
|
|
15362
|
+
packages: external_exports.array(external_exports.string()).optional(),
|
|
15363
|
+
advanced: external_exports.boolean()
|
|
15364
|
+
}),
|
|
15365
|
+
external_exports.object({
|
|
15366
|
+
type: external_exports.literal("stage.started"),
|
|
15367
|
+
stage: external_exports.enum(SCAN_STAGES),
|
|
15368
|
+
index: external_exports.number().int().nonnegative(),
|
|
15369
|
+
total: external_exports.number().int().positive(),
|
|
15370
|
+
checks: external_exports.array(external_exports.object({ checkId: external_exports.string().min(1), title: external_exports.string().min(1) }))
|
|
15371
|
+
}),
|
|
15372
|
+
external_exports.object({ type: external_exports.literal("stage.done"), stage: external_exports.enum(SCAN_STAGES) }),
|
|
15373
|
+
external_exports.object({
|
|
15374
|
+
type: external_exports.literal("check.done"),
|
|
15375
|
+
checkId: external_exports.string().min(1),
|
|
15376
|
+
title: external_exports.string().min(1),
|
|
15377
|
+
category: external_exports.enum(CATEGORIES),
|
|
15378
|
+
nodeId,
|
|
15379
|
+
outcome: external_exports.enum(CHECK_OUTCOMES),
|
|
15380
|
+
reason: external_exports.enum(SKIP_REASONS).optional()
|
|
15381
|
+
})
|
|
15382
|
+
]);
|
|
15383
|
+
var sequencedScanEventSchema = external_exports.intersection(
|
|
15384
|
+
scanEventSchema,
|
|
15385
|
+
external_exports.object({ seq: external_exports.number().int().nonnegative() })
|
|
15386
|
+
);
|
|
15387
|
+
|
|
15388
|
+
// src/consent.ts
|
|
15389
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
15390
|
+
import { createInterface } from "readline/promises";
|
|
15391
|
+
import { join as join2, resolve } from "path";
|
|
14979
15392
|
|
|
14980
15393
|
// src/token.ts
|
|
14981
15394
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14982
15395
|
import { homedir } from "os";
|
|
14983
|
-
import { join
|
|
15396
|
+
import { join } from "path";
|
|
14984
15397
|
function dirConfig() {
|
|
14985
|
-
return
|
|
15398
|
+
return join(homedir(), ".config", "vibecheck");
|
|
14986
15399
|
}
|
|
14987
15400
|
function caminhoToken() {
|
|
14988
|
-
return
|
|
15401
|
+
return join(dirConfig(), "token");
|
|
14989
15402
|
}
|
|
14990
15403
|
function lerToken() {
|
|
14991
15404
|
const env = process.env["VIBECHECK_TOKEN"];
|
|
@@ -15012,7 +15425,515 @@ ${host}
|
|
|
15012
15425
|
chmodSync(destino, 384);
|
|
15013
15426
|
}
|
|
15014
15427
|
|
|
15428
|
+
// src/consent.ts
|
|
15429
|
+
var ARQUIVO = "code-consent.json";
|
|
15430
|
+
var ENV_CI_ALLOW = "VIBECHECK_ALLOW_CODE_SCAN";
|
|
15431
|
+
function caminho(deps) {
|
|
15432
|
+
return join2((deps.configDir ?? dirConfig)(), ARQUIVO);
|
|
15433
|
+
}
|
|
15434
|
+
function repoKey(dir) {
|
|
15435
|
+
return resolve(dir);
|
|
15436
|
+
}
|
|
15437
|
+
function listarConsentimentos(deps = {}) {
|
|
15438
|
+
try {
|
|
15439
|
+
const bruto = JSON.parse(readFileSync2(caminho(deps), "utf8"));
|
|
15440
|
+
if (!Array.isArray(bruto)) return [];
|
|
15441
|
+
return bruto.filter(
|
|
15442
|
+
(e) => e !== null && typeof e === "object" && typeof e.repo === "string" && typeof e.acceptedAt === "string"
|
|
15443
|
+
);
|
|
15444
|
+
} catch {
|
|
15445
|
+
return [];
|
|
15446
|
+
}
|
|
15447
|
+
}
|
|
15448
|
+
function gravar(entradas, deps) {
|
|
15449
|
+
const dir = (deps.configDir ?? dirConfig)();
|
|
15450
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
15451
|
+
const arquivo = join2(dir, ARQUIVO);
|
|
15452
|
+
writeFileSync2(arquivo, `${JSON.stringify(entradas, null, 2)}
|
|
15453
|
+
`, { mode: 384 });
|
|
15454
|
+
chmodSync2(arquivo, 384);
|
|
15455
|
+
}
|
|
15456
|
+
function temConsentimento(dir, deps = {}) {
|
|
15457
|
+
const chave = repoKey(dir);
|
|
15458
|
+
return listarConsentimentos(deps).some((e) => e.repo === chave);
|
|
15459
|
+
}
|
|
15460
|
+
function registrarConsentimento(dir, deps = {}) {
|
|
15461
|
+
const chave = repoKey(dir);
|
|
15462
|
+
const atuais = listarConsentimentos(deps).filter((e) => e.repo !== chave);
|
|
15463
|
+
const quando = (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
15464
|
+
gravar([...atuais, { repo: chave, acceptedAt: quando }], deps);
|
|
15465
|
+
}
|
|
15466
|
+
function revogarConsentimento(dir, deps = {}) {
|
|
15467
|
+
const chave = repoKey(dir);
|
|
15468
|
+
const atuais = listarConsentimentos(deps);
|
|
15469
|
+
const restantes = atuais.filter((e) => e.repo !== chave);
|
|
15470
|
+
if (restantes.length === atuais.length) return false;
|
|
15471
|
+
gravar(restantes, deps);
|
|
15472
|
+
return true;
|
|
15473
|
+
}
|
|
15474
|
+
var TEXTO_DO_PEDIDO = [
|
|
15475
|
+
"VibeCheck is about to read the source files in this repository, on this machine,",
|
|
15476
|
+
"to run the code checks.",
|
|
15477
|
+
"",
|
|
15478
|
+
"What leaves your machine: the check id, its severity, the file path and the",
|
|
15479
|
+
"catalog sentence. Never a line of code, never a snippet, never a value.",
|
|
15480
|
+
""
|
|
15481
|
+
].join("\n");
|
|
15482
|
+
async function resolverConsentimento(dir, opcoes, deps = {}) {
|
|
15483
|
+
if (temConsentimento(dir, deps)) return "already";
|
|
15484
|
+
const env = deps.env ?? process.env;
|
|
15485
|
+
if (opcoes.ci) {
|
|
15486
|
+
if (env[ENV_CI_ALLOW] !== "1") return "needs-ci-env";
|
|
15487
|
+
registrarConsentimento(dir, deps);
|
|
15488
|
+
return "granted";
|
|
15489
|
+
}
|
|
15490
|
+
const perguntar = deps.ask ?? (async (pergunta) => {
|
|
15491
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
15492
|
+
try {
|
|
15493
|
+
return await rl.question(pergunta);
|
|
15494
|
+
} finally {
|
|
15495
|
+
rl.close();
|
|
15496
|
+
}
|
|
15497
|
+
});
|
|
15498
|
+
const resposta = await perguntar(`${TEXTO_DO_PEDIDO}Read this repository? [y/N] `);
|
|
15499
|
+
if (!/^y(es)?$/i.test(resposta.trim())) return "denied";
|
|
15500
|
+
registrarConsentimento(dir, deps);
|
|
15501
|
+
return "granted";
|
|
15502
|
+
}
|
|
15503
|
+
|
|
15504
|
+
// src/follow.ts
|
|
15505
|
+
function eventsUrl(base, scanId, shareToken) {
|
|
15506
|
+
return `${base}/api/scan/${scanId}/events?t=${encodeURIComponent(shareToken)}`;
|
|
15507
|
+
}
|
|
15508
|
+
async function followScan(base, scanId, shareToken, options = {}) {
|
|
15509
|
+
const fetchFn = options.fetch ?? fetch;
|
|
15510
|
+
const events = [];
|
|
15511
|
+
let exit = 0;
|
|
15512
|
+
const resposta = await fetchFn(eventsUrl(base, scanId, shareToken));
|
|
15513
|
+
if (!resposta.ok || resposta.body === null) {
|
|
15514
|
+
return { events, final: null, exit: 0, opened: false };
|
|
15515
|
+
}
|
|
15516
|
+
const leitor = resposta.body.getReader();
|
|
15517
|
+
const decoder = new TextDecoder();
|
|
15518
|
+
let buf = "";
|
|
15519
|
+
while (true) {
|
|
15520
|
+
const { done, value } = await leitor.read();
|
|
15521
|
+
if (done) break;
|
|
15522
|
+
buf += decoder.decode(value, { stream: true });
|
|
15523
|
+
const blocos = buf.split("\n\n");
|
|
15524
|
+
buf = blocos.pop() ?? "";
|
|
15525
|
+
for (const bloco of blocos) {
|
|
15526
|
+
const linha = bloco.split("\n").find((l) => l.startsWith("data: "));
|
|
15527
|
+
if (linha === void 0) continue;
|
|
15528
|
+
let evento;
|
|
15529
|
+
try {
|
|
15530
|
+
evento = JSON.parse(linha.slice(6));
|
|
15531
|
+
} catch {
|
|
15532
|
+
continue;
|
|
15533
|
+
}
|
|
15534
|
+
if (typeof evento !== "object" || evento === null || !("type" in evento)) continue;
|
|
15535
|
+
events.push(evento);
|
|
15536
|
+
options.onEvent?.(evento);
|
|
15537
|
+
if (evento.type === "finding.found" && (evento.severity === "fatal" || evento.severity === "critical")) {
|
|
15538
|
+
exit = 1;
|
|
15539
|
+
}
|
|
15540
|
+
if (evento.type === "scan.done" || evento.type === "scan.failed") {
|
|
15541
|
+
if (evento.type === "scan.failed") exit = 1;
|
|
15542
|
+
return { events, final: evento, exit, opened: true };
|
|
15543
|
+
}
|
|
15544
|
+
}
|
|
15545
|
+
}
|
|
15546
|
+
return { events, final: null, exit, opened: true };
|
|
15547
|
+
}
|
|
15548
|
+
|
|
15549
|
+
// src/gate.ts
|
|
15550
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
15551
|
+
import { join as join3 } from "path";
|
|
15552
|
+
|
|
15553
|
+
// ../../packages/checks/src/code/types.ts
|
|
15554
|
+
var API_ROUTE_FILE = /(^|\/)(app\/.*\/route\.(ts|js|tsx|jsx)|pages\/api\/.+\.(ts|js))$/;
|
|
15555
|
+
function isApiRoute(path) {
|
|
15556
|
+
return API_ROUTE_FILE.test(path);
|
|
15557
|
+
}
|
|
15558
|
+
|
|
15559
|
+
// ../../packages/checks/src/code/checks.ts
|
|
15560
|
+
var MIGRATION_FILE = /(^|\/)supabase\/migrations\/[^/]+\.sql$/;
|
|
15561
|
+
var CREATE_TABLE = /create\s+table\s+(?:if\s+not\s+exists\s+)?(?:"?[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/gi;
|
|
15562
|
+
var ENABLE_RLS = /alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:"?[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?\s+enable\s+row\s+level\s+security/gi;
|
|
15563
|
+
function all(pattern, text) {
|
|
15564
|
+
const out = [];
|
|
15565
|
+
for (const m of text.matchAll(pattern)) {
|
|
15566
|
+
const nome = m[1];
|
|
15567
|
+
if (nome !== void 0) out.push(nome.toLowerCase());
|
|
15568
|
+
}
|
|
15569
|
+
return out;
|
|
15570
|
+
}
|
|
15571
|
+
function isClientFile(file2) {
|
|
15572
|
+
if (/^\s*['"]use client['"]/.test(file2.content)) return true;
|
|
15573
|
+
if (/\.client\.(tsx|jsx|ts|js)$/.test(file2.path)) return true;
|
|
15574
|
+
return /(^|\/)pages\/(?!api\/).+\.(tsx|jsx|ts|js)$/.test(file2.path);
|
|
15575
|
+
}
|
|
15576
|
+
var TOUCHES_DB = /\b(supabase\.(?:from|rpc|storage)\b|prisma\.\w+\.\w+\(|drizzle\(|\bdb\.(?:select|insert|update|delete|query|execute|from)\(|\.from\(['"]|sql`|pool\.query\(|knex\(|mongoose\.|\.collection\(['"])/;
|
|
15577
|
+
var HAS_AUTH = /\b(getSession|getUser|auth\(\)|currentUser|requireAuth|verifyToken|verifyJwt|jwtVerify|authorization|Bearer|withAuth|clerk|next-auth|getServerSession|session)\b/i;
|
|
15578
|
+
var PUBLIC_ROUTE = /(^|\/)(webhooks?|health|healthz|ping|status|robots|sitemap|og|opengraph|cron)(\/|\.|$)/i;
|
|
15579
|
+
var AI_SDK = /from\s+['"](openai|@anthropic-ai\/sdk|ai|@ai-sdk\/[^'"]+|@google\/generative-ai|groq-sdk|cohere-ai|@mistralai\/mistralai)['"]/;
|
|
15580
|
+
var HAS_LIMITER = /\b(ratelimit|rateLimit|Ratelimit|upstash|limiter|throttle|slidingWindow|tokenBucket)\b/;
|
|
15581
|
+
var STRIPE_IMPORT = /from\s+['"]stripe['"]/;
|
|
15582
|
+
var WEBHOOK_PATH = /webhook/i;
|
|
15583
|
+
var SIGNATURE_CHECK = /constructEvent(?:Async)?\(|stripe-signature/;
|
|
15584
|
+
var SERVICE_ROLE_REF = /\b(SUPABASE_SERVICE_ROLE_KEY|SERVICE_ROLE_KEY|sb_secret_[A-Za-z0-9_]+|NEXT_PUBLIC_[A-Z0-9_]*(?:SECRET|SERVICE_ROLE|PRIVATE)[A-Z0-9_]*)\b/;
|
|
15585
|
+
var ENV_FILES = [
|
|
15586
|
+
".env",
|
|
15587
|
+
".env.local",
|
|
15588
|
+
".env.production",
|
|
15589
|
+
".env.development",
|
|
15590
|
+
".env.staging"
|
|
15591
|
+
];
|
|
15592
|
+
var codeRlsMissing = {
|
|
15593
|
+
id: "code-rls-missing",
|
|
15594
|
+
source: "code",
|
|
15595
|
+
category: "data",
|
|
15596
|
+
severity: "critical",
|
|
15597
|
+
title: "Table created without row level security",
|
|
15598
|
+
impact: "A migration creates a table and never turns on row level security for it. Once it has rows, anyone with the public key can read them \u2014 this is the hole the RLS check lights up from outside.",
|
|
15599
|
+
howToDetect: (ctx) => {
|
|
15600
|
+
const migrations = ctx.files.filter((f) => MIGRATION_FILE.test(f.path));
|
|
15601
|
+
if (migrations.length === 0) return [];
|
|
15602
|
+
const comRls = /* @__PURE__ */ new Set();
|
|
15603
|
+
for (const m of migrations) for (const t of all(ENABLE_RLS, m.content)) comRls.add(t);
|
|
15604
|
+
const out = [];
|
|
15605
|
+
const vistas = /* @__PURE__ */ new Set();
|
|
15606
|
+
for (const m of migrations) {
|
|
15607
|
+
for (const tabela of all(CREATE_TABLE, m.content)) {
|
|
15608
|
+
if (comRls.has(tabela) || vistas.has(tabela)) continue;
|
|
15609
|
+
vistas.add(tabela);
|
|
15610
|
+
out.push({
|
|
15611
|
+
path: m.path,
|
|
15612
|
+
reason: `table \`${tabela}\` is created without enable row level security`
|
|
15613
|
+
});
|
|
15614
|
+
}
|
|
15615
|
+
}
|
|
15616
|
+
return out;
|
|
15617
|
+
},
|
|
15618
|
+
fixPrompt: (a) => `In your code, ${a.reason} (${a.path}).
|
|
15619
|
+
|
|
15620
|
+
We do not know the columns or who should read what. We cannot write the policy from here.
|
|
15621
|
+
|
|
15622
|
+
Good practice: every table starts with row level security on, in the same migration that creates it. Then one policy per real access: a person reads their own rows; a public catalog, if it exists, is a deliberate read policy; writes always go through a session. Do this in a new migration, never by editing one that already ran.
|
|
15623
|
+
|
|
15624
|
+
After, deploy and read the table with the public key and no session \u2014 it must answer empty or denied.`
|
|
15625
|
+
};
|
|
15626
|
+
var codeServiceRoleInClient = {
|
|
15627
|
+
id: "code-service-role-in-client",
|
|
15628
|
+
source: "code",
|
|
15629
|
+
category: "secrets",
|
|
15630
|
+
severity: "fatal",
|
|
15631
|
+
title: "Service key referenced in client code",
|
|
15632
|
+
impact: "A file that runs in the browser mentions the Supabase service key or a NEXT_PUBLIC_ secret. Whatever the bundler inlines there ships to every visitor \u2014 this is the fatal finding from outside, one build away.",
|
|
15633
|
+
howToDetect: (ctx) => ctx.files.filter(isClientFile).filter((f) => SERVICE_ROLE_REF.test(f.content)).map((f) => ({
|
|
15634
|
+
path: f.path,
|
|
15635
|
+
reason: "client-side file references a service or secret key"
|
|
15636
|
+
})),
|
|
15637
|
+
fixPrompt: (a) => `In your code, a client-side file references a service or secret key (${a.path}).
|
|
15638
|
+
|
|
15639
|
+
We do not have the value and we do not want it. The problem is the place, not the string.
|
|
15640
|
+
|
|
15641
|
+
Good practice: the service key lives only in server code \u2014 a route handler, a server action, an edge function \u2014 read from the environment without the NEXT_PUBLIC_ prefix. The browser talks to your API; your API talks to the database. Remove the reference from the client file and, if the key was ever bundled, rotate it in the dashboard.
|
|
15642
|
+
|
|
15643
|
+
After, search the built bundle for the key prefix and confirm it is gone.`
|
|
15644
|
+
};
|
|
15645
|
+
var codeRouteWithoutAuth = {
|
|
15646
|
+
id: "code-route-without-auth",
|
|
15647
|
+
source: "code",
|
|
15648
|
+
category: "auth",
|
|
15649
|
+
severity: "high",
|
|
15650
|
+
title: "API route touches data without a session check",
|
|
15651
|
+
impact: "An API route reads or writes the database and nothing in it checks who is asking. If the middleware does not cover it, anyone can call it \u2014 this is what api-route-noauth lights up from outside.",
|
|
15652
|
+
howToDetect: (ctx) => ctx.files.filter((f) => isApiRoute(f.path) && !PUBLIC_ROUTE.test(f.path)).filter((f) => TOUCHES_DB.test(f.content) && !HAS_AUTH.test(f.content)).map((f) => ({
|
|
15653
|
+
path: f.path,
|
|
15654
|
+
reason: "route reads or writes data with no session check in the file"
|
|
15655
|
+
})),
|
|
15656
|
+
fixPrompt: (a) => `In your code, a route reads or writes data with no session check in the file (${a.path}).
|
|
15657
|
+
|
|
15658
|
+
We cannot see your middleware. If it already protects this path, mark the finding as known.
|
|
15659
|
+
|
|
15660
|
+
Good practice: the route resolves the session first and answers 401 before touching data; what it reads or writes is scoped to that user. A public route is public on purpose, with a comment saying so, and never exposes rows that belong to someone.
|
|
15661
|
+
|
|
15662
|
+
After, call the route from a logged-out client and confirm it answers 401 or 403.`
|
|
15663
|
+
};
|
|
15664
|
+
var codeEnvCommitted = {
|
|
15665
|
+
id: "code-env-committed",
|
|
15666
|
+
source: "code",
|
|
15667
|
+
category: "secrets",
|
|
15668
|
+
severity: "critical",
|
|
15669
|
+
title: ".env file tracked by git",
|
|
15670
|
+
impact: "An environment file is committed to the repository. Every clone, fork, CI log and AI tool that reads the repo has the secrets in it. Even after deleting it, the history keeps the values.",
|
|
15671
|
+
// Só o nome. O conteúdo nunca é lido — é o único check que não recebe
|
|
15672
|
+
// `content`, e é assim de propósito.
|
|
15673
|
+
howToDetect: (ctx) => ENV_FILES.filter((nome) => ctx.tracked(nome)).map((nome) => ({
|
|
15674
|
+
path: nome,
|
|
15675
|
+
reason: `${nome} is tracked by git`
|
|
15676
|
+
})),
|
|
15677
|
+
fixPrompt: (a) => `In your code, ${a.reason}.
|
|
15678
|
+
|
|
15679
|
+
We did not read it. We only saw that git tracks it.
|
|
15680
|
+
|
|
15681
|
+
Good practice: environment files stay out of version control \u2014 add them to .gitignore, remove them from the index, and keep a committed .env.example with the variable names and no values. Every secret that was ever in the file is compromised: rotate each one in its provider.
|
|
15682
|
+
|
|
15683
|
+
After, run git ls-files and confirm no .env file is listed.`
|
|
15684
|
+
};
|
|
15685
|
+
var codeAiRouteWithoutLimit = {
|
|
15686
|
+
id: "code-ai-route-without-limit",
|
|
15687
|
+
source: "code",
|
|
15688
|
+
category: "ai",
|
|
15689
|
+
severity: "high",
|
|
15690
|
+
title: "AI route with no rate limit",
|
|
15691
|
+
impact: "A route calls an LLM provider and nothing in it limits how often. A loop from one client turns your provider account into an open bill \u2014 this is ai-proxy-open, seen from inside.",
|
|
15692
|
+
howToDetect: (ctx) => ctx.files.filter((f) => isApiRoute(f.path)).filter((f) => AI_SDK.test(f.content) && !HAS_LIMITER.test(f.content)).map((f) => ({
|
|
15693
|
+
path: f.path,
|
|
15694
|
+
reason: "route calls an AI provider with no rate limit in the file"
|
|
15695
|
+
})),
|
|
15696
|
+
fixPrompt: (a) => `In your code, a route calls an AI provider with no rate limit in the file (${a.path}).
|
|
15697
|
+
|
|
15698
|
+
We cannot see limits applied elsewhere. If your gateway already throttles this path, mark the finding as known.
|
|
15699
|
+
|
|
15700
|
+
Good practice: the route requires a session, then applies a per-user limit (so many calls per minute, so many tokens per day) before calling the provider, and answers 429 when it is hit. Cost caps at the provider are the second net, not the first.
|
|
15701
|
+
|
|
15702
|
+
After, call the route in a loop from one client and confirm it starts answering 429.`
|
|
15703
|
+
};
|
|
15704
|
+
var codeWebhookUnsigned = {
|
|
15705
|
+
id: "code-webhook-unsigned",
|
|
15706
|
+
source: "code",
|
|
15707
|
+
category: "config",
|
|
15708
|
+
severity: "high",
|
|
15709
|
+
title: "Stripe webhook without signature verification",
|
|
15710
|
+
impact: 'A webhook route imports Stripe and never verifies the event signature. Anyone can POST a fake "payment succeeded" and your app will believe it \u2014 the one check we refuse to run from outside, because proving it would mean doing exactly that.',
|
|
15711
|
+
howToDetect: (ctx) => ctx.files.filter((f) => isApiRoute(f.path) && WEBHOOK_PATH.test(f.path)).filter((f) => STRIPE_IMPORT.test(f.content) && !SIGNATURE_CHECK.test(f.content)).map((f) => ({
|
|
15712
|
+
path: f.path,
|
|
15713
|
+
reason: "Stripe webhook route never verifies the event signature"
|
|
15714
|
+
})),
|
|
15715
|
+
fixPrompt: (a) => `In your code, a Stripe webhook route never verifies the event signature (${a.path}).
|
|
15716
|
+
|
|
15717
|
+
We cannot see the secret and do not want it.
|
|
15718
|
+
|
|
15719
|
+
Good practice: the route reads the raw body and the stripe-signature header, builds the event with the webhook secret from the environment, and rejects anything that fails. Only the verified event drives state. Each event id is processed once.
|
|
15720
|
+
|
|
15721
|
+
After, POST a fabricated event to the route and confirm it answers 400.`
|
|
15722
|
+
};
|
|
15723
|
+
var CODE_CHECKS = [
|
|
15724
|
+
codeRlsMissing,
|
|
15725
|
+
codeServiceRoleInClient,
|
|
15726
|
+
codeRouteWithoutAuth,
|
|
15727
|
+
codeEnvCommitted,
|
|
15728
|
+
codeAiRouteWithoutLimit,
|
|
15729
|
+
codeWebhookUnsigned
|
|
15730
|
+
];
|
|
15731
|
+
function runCodeChecks(ctx, catalog = CODE_CHECKS) {
|
|
15732
|
+
const out = [];
|
|
15733
|
+
for (const check2 of catalog) {
|
|
15734
|
+
let advisories = [];
|
|
15735
|
+
try {
|
|
15736
|
+
advisories = check2.howToDetect(ctx);
|
|
15737
|
+
} catch {
|
|
15738
|
+
advisories = [];
|
|
15739
|
+
}
|
|
15740
|
+
for (const a of advisories) {
|
|
15741
|
+
out.push({
|
|
15742
|
+
checkId: check2.id,
|
|
15743
|
+
severity: check2.severity,
|
|
15744
|
+
path: a.path,
|
|
15745
|
+
reason: a.reason
|
|
15746
|
+
});
|
|
15747
|
+
}
|
|
15748
|
+
}
|
|
15749
|
+
return out;
|
|
15750
|
+
}
|
|
15751
|
+
|
|
15752
|
+
// src/gate.ts
|
|
15753
|
+
var GATE_DIR = ".vibecheck";
|
|
15754
|
+
var GATE_JSON = "quality-gate.json";
|
|
15755
|
+
var GATE_SARIF = "quality-gate.sarif";
|
|
15756
|
+
function escreverArtefatos(dir, report, sarif, deps) {
|
|
15757
|
+
deps.writeArtifact(
|
|
15758
|
+
join3(dir, GATE_DIR, GATE_JSON),
|
|
15759
|
+
`${JSON.stringify(report, null, 2)}
|
|
15760
|
+
`
|
|
15761
|
+
);
|
|
15762
|
+
deps.writeArtifact(
|
|
15763
|
+
join3(dir, GATE_DIR, GATE_SARIF),
|
|
15764
|
+
`${JSON.stringify(sarif, null, 2)}
|
|
15765
|
+
`
|
|
15766
|
+
);
|
|
15767
|
+
}
|
|
15768
|
+
function gravarNoDisco(caminho2, conteudo) {
|
|
15769
|
+
mkdirSync3(join3(caminho2, ".."), { recursive: true });
|
|
15770
|
+
writeFileSync3(caminho2, conteudo);
|
|
15771
|
+
}
|
|
15772
|
+
async function rodarHttp(input, grafo, deps) {
|
|
15773
|
+
const headers = {
|
|
15774
|
+
authorization: `Bearer ${input.token}`,
|
|
15775
|
+
"content-type": "application/json"
|
|
15776
|
+
};
|
|
15777
|
+
const criado = await deps.fetch(`${deps.base}/api/cli/scan`, {
|
|
15778
|
+
method: "POST",
|
|
15779
|
+
headers,
|
|
15780
|
+
body: JSON.stringify({
|
|
15781
|
+
url: input.url,
|
|
15782
|
+
...grafo === null ? {} : { graph: grafo }
|
|
15783
|
+
})
|
|
15784
|
+
});
|
|
15785
|
+
const corpo = await criado.json().catch(() => ({}));
|
|
15786
|
+
if (criado.status === 429) {
|
|
15787
|
+
deps.log(`quota: ${corpo.error ?? "this domain was scanned recently"}`);
|
|
15788
|
+
return { outcome: "skipped-quota", issues: [] };
|
|
15789
|
+
}
|
|
15790
|
+
if (!criado.ok || corpo.scanId === void 0) {
|
|
15791
|
+
deps.log(`scan not opened: ${corpo.error ?? `HTTP ${String(criado.status)}`}`);
|
|
15792
|
+
return { outcome: "failed", issues: [] };
|
|
15793
|
+
}
|
|
15794
|
+
const scan = {
|
|
15795
|
+
scanId: corpo.scanId,
|
|
15796
|
+
shareToken: corpo.shareToken ?? "",
|
|
15797
|
+
liveUrl: corpo.liveUrl ?? `${deps.base}/advanced/${corpo.scanId}`
|
|
15798
|
+
};
|
|
15799
|
+
if (grafo !== null && grafo.advanced) {
|
|
15800
|
+
const put = await deps.fetch(`${deps.base}/api/cli/scan/${scan.scanId}/graph`, {
|
|
15801
|
+
method: "PUT",
|
|
15802
|
+
headers,
|
|
15803
|
+
body: JSON.stringify(grafo)
|
|
15804
|
+
});
|
|
15805
|
+
if (!put.ok) deps.log("graph rejected by the server \u2014 scan continues HTTP-only");
|
|
15806
|
+
}
|
|
15807
|
+
const seguido = await followScan(deps.base, scan.scanId, scan.shareToken, {
|
|
15808
|
+
fetch: deps.fetch
|
|
15809
|
+
});
|
|
15810
|
+
if (!seguido.opened) {
|
|
15811
|
+
deps.log(`could not follow the scan \u2014 see ${scan.liveUrl}`);
|
|
15812
|
+
return { outcome: "failed", issues: [], scan };
|
|
15813
|
+
}
|
|
15814
|
+
const issues = [];
|
|
15815
|
+
for (const evento of seguido.events) {
|
|
15816
|
+
if (evento.type !== "finding.found") continue;
|
|
15817
|
+
issues.push({
|
|
15818
|
+
checkId: evento.checkId,
|
|
15819
|
+
severity: evento.severity,
|
|
15820
|
+
// No achado HTTP o "path" é o assunto (tabela, bucket, rota) ou o
|
|
15821
|
+
// nó que acendeu — não há arquivo do lado de fora.
|
|
15822
|
+
path: evento.subject ?? evento.nodeId,
|
|
15823
|
+
reason: evento.proof.length > 0 ? evento.proof : evento.checkId,
|
|
15824
|
+
source: "http"
|
|
15825
|
+
});
|
|
15826
|
+
}
|
|
15827
|
+
return {
|
|
15828
|
+
outcome: seguido.final?.type === "scan.failed" ? "failed" : "ran",
|
|
15829
|
+
issues,
|
|
15830
|
+
scan
|
|
15831
|
+
};
|
|
15832
|
+
}
|
|
15833
|
+
async function runGate(input, deps) {
|
|
15834
|
+
const decisao = await resolverConsentimento(input.dir, { ci: input.ci }, deps.consent);
|
|
15835
|
+
if (decisao === "denied") {
|
|
15836
|
+
deps.log("code scan declined \u2014 nothing was read, nothing was sent");
|
|
15837
|
+
return { exit: GATE_EXIT.inconclusive, report: null };
|
|
15838
|
+
}
|
|
15839
|
+
if (decisao === "needs-ci-env") {
|
|
15840
|
+
deps.log(
|
|
15841
|
+
"CI needs VIBECHECK_ALLOW_CODE_SCAN=1 to read the repository \u2014 refusing to guess"
|
|
15842
|
+
);
|
|
15843
|
+
return { exit: GATE_EXIT.inconclusive, report: null };
|
|
15844
|
+
}
|
|
15845
|
+
const grafo = await deps.extractGraph(input.dir);
|
|
15846
|
+
const metrics = grafo === null ? null : graphMetrics(grafo);
|
|
15847
|
+
const repo = deps.readRepo(input.dir);
|
|
15848
|
+
const codeIssues = runCodeChecks(repo).map((i) => ({
|
|
15849
|
+
checkId: i.checkId,
|
|
15850
|
+
severity: i.severity,
|
|
15851
|
+
path: i.path,
|
|
15852
|
+
reason: i.reason,
|
|
15853
|
+
source: "code"
|
|
15854
|
+
}));
|
|
15855
|
+
let httpScan = "skipped-flag";
|
|
15856
|
+
let httpIssues = [];
|
|
15857
|
+
let scan;
|
|
15858
|
+
if (!input.codeOnly) {
|
|
15859
|
+
const resultado = await rodarHttp(input, grafo, deps);
|
|
15860
|
+
httpScan = resultado.outcome;
|
|
15861
|
+
httpIssues = resultado.issues;
|
|
15862
|
+
scan = resultado.scan;
|
|
15863
|
+
}
|
|
15864
|
+
const issues = [...codeIssues, ...httpIssues];
|
|
15865
|
+
const codeFiles = repo.files.length;
|
|
15866
|
+
const report = {
|
|
15867
|
+
policy: QUALITY_GATE_POLICY,
|
|
15868
|
+
status: statusFromIssues(issues, httpScan, analysed({ codeFiles, httpScan })),
|
|
15869
|
+
httpScan,
|
|
15870
|
+
issues,
|
|
15871
|
+
counts: contarPorSeveridade(issues),
|
|
15872
|
+
metrics,
|
|
15873
|
+
codeFiles,
|
|
15874
|
+
ranAt: deps.now().toISOString()
|
|
15875
|
+
};
|
|
15876
|
+
return {
|
|
15877
|
+
exit: exitCodeFromStatus(report.status),
|
|
15878
|
+
report,
|
|
15879
|
+
...scan === void 0 ? {} : { scanId: scan.scanId, liveUrl: scan.liveUrl }
|
|
15880
|
+
};
|
|
15881
|
+
}
|
|
15882
|
+
async function enviarGate(scanId, report, input, deps) {
|
|
15883
|
+
const res = await deps.fetch(`${deps.base}/api/cli/scan/${scanId}/quality-gate`, {
|
|
15884
|
+
method: "PUT",
|
|
15885
|
+
headers: {
|
|
15886
|
+
authorization: `Bearer ${input.token}`,
|
|
15887
|
+
"content-type": "application/json"
|
|
15888
|
+
},
|
|
15889
|
+
body: JSON.stringify(report)
|
|
15890
|
+
});
|
|
15891
|
+
if (!res.ok) {
|
|
15892
|
+
const corpo = await res.json().catch(() => ({}));
|
|
15893
|
+
deps.log(`gate not saved: ${corpo.error ?? `HTTP ${String(res.status)}`}`);
|
|
15894
|
+
return false;
|
|
15895
|
+
}
|
|
15896
|
+
return true;
|
|
15897
|
+
}
|
|
15898
|
+
|
|
15899
|
+
// src/graphify.ts
|
|
15900
|
+
import { spawn } from "child_process";
|
|
15901
|
+
import { readFile } from "fs/promises";
|
|
15902
|
+
import { join as join4 } from "path";
|
|
15903
|
+
async function tentarGraphify(raiz) {
|
|
15904
|
+
const json2 = join4(raiz, "graphify-out", "graph.json");
|
|
15905
|
+
try {
|
|
15906
|
+
return JSON.parse(await readFile(json2, "utf8"));
|
|
15907
|
+
} catch {
|
|
15908
|
+
}
|
|
15909
|
+
const ok = await new Promise((resolve2) => {
|
|
15910
|
+
const child = spawn("graphify", [".", "--no-viz"], {
|
|
15911
|
+
cwd: raiz,
|
|
15912
|
+
stdio: "ignore"
|
|
15913
|
+
});
|
|
15914
|
+
const timer = setTimeout(() => {
|
|
15915
|
+
child.kill();
|
|
15916
|
+
resolve2(false);
|
|
15917
|
+
}, 2e4);
|
|
15918
|
+
child.on("exit", (code) => {
|
|
15919
|
+
clearTimeout(timer);
|
|
15920
|
+
resolve2(code === 0);
|
|
15921
|
+
});
|
|
15922
|
+
child.on("error", () => {
|
|
15923
|
+
clearTimeout(timer);
|
|
15924
|
+
resolve2(false);
|
|
15925
|
+
});
|
|
15926
|
+
});
|
|
15927
|
+
if (!ok) return null;
|
|
15928
|
+
try {
|
|
15929
|
+
return JSON.parse(await readFile(json2, "utf8"));
|
|
15930
|
+
} catch {
|
|
15931
|
+
return null;
|
|
15932
|
+
}
|
|
15933
|
+
}
|
|
15934
|
+
|
|
15015
15935
|
// src/login.ts
|
|
15936
|
+
import { spawn as spawn2 } from "child_process";
|
|
15016
15937
|
function urlBase() {
|
|
15017
15938
|
return (process.env["VIBECHECK_URL"] ?? "https://vibecheck.kinkai.cloud").replace(
|
|
15018
15939
|
/\/$/,
|
|
@@ -15032,12 +15953,12 @@ function comandosAbrirBrowser(url2, platform = process.platform, env = process.e
|
|
|
15032
15953
|
return [["xdg-open", [url2]]];
|
|
15033
15954
|
}
|
|
15034
15955
|
function tentarSpawn(cmd, args) {
|
|
15035
|
-
return new Promise((
|
|
15956
|
+
return new Promise((resolve2) => {
|
|
15036
15957
|
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
15037
|
-
child.once("error", () =>
|
|
15958
|
+
child.once("error", () => resolve2(false));
|
|
15038
15959
|
child.once("spawn", () => {
|
|
15039
15960
|
child.unref();
|
|
15040
|
-
|
|
15961
|
+
resolve2(true);
|
|
15041
15962
|
});
|
|
15042
15963
|
});
|
|
15043
15964
|
}
|
|
@@ -15058,22 +15979,23 @@ async function detalheHttp(resposta) {
|
|
|
15058
15979
|
}
|
|
15059
15980
|
}
|
|
15060
15981
|
if (resposta.status === 404) {
|
|
15061
|
-
return " \u2014
|
|
15982
|
+
return " \u2014 the CLI API is not in this deploy yet";
|
|
15062
15983
|
}
|
|
15063
15984
|
return "";
|
|
15064
15985
|
}
|
|
15065
|
-
async function loginDevice(ci) {
|
|
15986
|
+
async function loginDevice(ci, options = {}) {
|
|
15066
15987
|
const base = urlBase();
|
|
15067
15988
|
const aberto = await fetch(`${base}/api/cli/device`, { method: "POST" });
|
|
15068
15989
|
if (!aberto.ok) {
|
|
15069
15990
|
const extra = await detalheHttp(aberto);
|
|
15070
15991
|
throw new Error(
|
|
15071
|
-
`
|
|
15992
|
+
`could not open the device-code at ${base} (HTTP ${aberto.status})${extra}`
|
|
15072
15993
|
);
|
|
15073
15994
|
}
|
|
15074
15995
|
const corpo = await aberto.json();
|
|
15075
|
-
console.warn(`
|
|
15996
|
+
console.warn(`In the browser, sign in and authorize with code ${corpo.user_code}`);
|
|
15076
15997
|
console.warn(corpo.verification_url);
|
|
15998
|
+
options.onCode?.({ userCode: corpo.user_code, verificationUrl: corpo.verification_url });
|
|
15077
15999
|
if (!ci) await abrirBrowser(corpo.verification_url);
|
|
15078
16000
|
const intervalo = Math.max(3, corpo.interval ?? 3) * 1e3;
|
|
15079
16001
|
const limite = Date.now() + 10 * 6e4;
|
|
@@ -15088,15 +16010,133 @@ async function loginDevice(ci) {
|
|
|
15088
16010
|
return { token: dados.token, host: dados.host };
|
|
15089
16011
|
}
|
|
15090
16012
|
if (dados.status === "expired" || dados.error === "expired") {
|
|
15091
|
-
throw new Error("
|
|
16013
|
+
throw new Error("code expired \u2014 run again");
|
|
16014
|
+
}
|
|
16015
|
+
}
|
|
16016
|
+
throw new Error("timed out without authorization");
|
|
16017
|
+
}
|
|
16018
|
+
|
|
16019
|
+
// src/repo.ts
|
|
16020
|
+
import { execFileSync } from "child_process";
|
|
16021
|
+
import { lstatSync, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
16022
|
+
import { join as join5, relative } from "path";
|
|
16023
|
+
var MAX_FILES = 800;
|
|
16024
|
+
var MAX_BYTES2 = 64e3;
|
|
16025
|
+
var MAX_SQL_BYTES = 512e3;
|
|
16026
|
+
var SOURCE = /\.(ts|tsx|js|jsx|mjs|cjs|sql)$/;
|
|
16027
|
+
var SKIP_DIR = /* @__PURE__ */ new Set([
|
|
16028
|
+
"node_modules",
|
|
16029
|
+
".git",
|
|
16030
|
+
"dist",
|
|
16031
|
+
".next",
|
|
16032
|
+
"coverage",
|
|
16033
|
+
"graphify-out",
|
|
16034
|
+
".turbo",
|
|
16035
|
+
"out",
|
|
16036
|
+
"build",
|
|
16037
|
+
".vercel"
|
|
16038
|
+
]);
|
|
16039
|
+
function gitTracked(dir) {
|
|
16040
|
+
try {
|
|
16041
|
+
const saida = execFileSync("git", ["ls-files", "-z"], {
|
|
16042
|
+
cwd: dir,
|
|
16043
|
+
encoding: "utf8",
|
|
16044
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
16045
|
+
maxBuffer: 16 * 1024 * 1024
|
|
16046
|
+
});
|
|
16047
|
+
return new Set(saida.split("\0").filter((p) => p.length > 0));
|
|
16048
|
+
} catch {
|
|
16049
|
+
return null;
|
|
16050
|
+
}
|
|
16051
|
+
}
|
|
16052
|
+
function walk(dir, raiz, out) {
|
|
16053
|
+
if (out.length >= MAX_FILES) return;
|
|
16054
|
+
let entradas;
|
|
16055
|
+
try {
|
|
16056
|
+
entradas = readdirSync(dir);
|
|
16057
|
+
} catch {
|
|
16058
|
+
return;
|
|
16059
|
+
}
|
|
16060
|
+
for (const nome of entradas) {
|
|
16061
|
+
if (out.length >= MAX_FILES) return;
|
|
16062
|
+
if (SKIP_DIR.has(nome) || nome.startsWith(".") && nome !== ".env") continue;
|
|
16063
|
+
const caminho2 = join5(dir, nome);
|
|
16064
|
+
let st;
|
|
16065
|
+
try {
|
|
16066
|
+
st = lstatSync(caminho2);
|
|
16067
|
+
} catch {
|
|
16068
|
+
continue;
|
|
16069
|
+
}
|
|
16070
|
+
if (st.isSymbolicLink()) continue;
|
|
16071
|
+
if (st.isDirectory()) walk(caminho2, raiz, out);
|
|
16072
|
+
else if (SOURCE.test(nome)) out.push(relative(raiz, caminho2).split("\\").join("/"));
|
|
16073
|
+
}
|
|
16074
|
+
}
|
|
16075
|
+
function readCodeContext(dir) {
|
|
16076
|
+
const tracked = gitTracked(dir);
|
|
16077
|
+
const caminhos = [];
|
|
16078
|
+
walk(dir, dir, caminhos);
|
|
16079
|
+
const files = [];
|
|
16080
|
+
for (const path of caminhos) {
|
|
16081
|
+
try {
|
|
16082
|
+
const st = lstatSync(join5(dir, path));
|
|
16083
|
+
if (st.size > (path.endsWith(".sql") ? MAX_SQL_BYTES : MAX_BYTES2)) continue;
|
|
16084
|
+
files.push({ path, content: readFileSync3(join5(dir, path), "utf8") });
|
|
16085
|
+
} catch {
|
|
15092
16086
|
}
|
|
15093
16087
|
}
|
|
15094
|
-
|
|
16088
|
+
return {
|
|
16089
|
+
files,
|
|
16090
|
+
// Sem git não há "commitado": um `.env` só presente no disco era
|
|
16091
|
+
// reportado como "tracked by git" (critical) em qualquer pasta sem
|
|
16092
|
+
// `.git` — tarball, contexto de build, runner sem git no PATH.
|
|
16093
|
+
tracked: (path) => tracked !== null && tracked.has(path)
|
|
16094
|
+
};
|
|
16095
|
+
}
|
|
16096
|
+
|
|
16097
|
+
// src/sarif.ts
|
|
16098
|
+
var NIVEL = {
|
|
16099
|
+
fatal: "error",
|
|
16100
|
+
critical: "error",
|
|
16101
|
+
high: "error",
|
|
16102
|
+
medium: "warning",
|
|
16103
|
+
low: "note"
|
|
16104
|
+
};
|
|
16105
|
+
function regras(issues) {
|
|
16106
|
+
const vistos = /* @__PURE__ */ new Map();
|
|
16107
|
+
for (const issue2 of issues) {
|
|
16108
|
+
if (!vistos.has(issue2.checkId)) vistos.set(issue2.checkId, issue2.reason);
|
|
16109
|
+
}
|
|
16110
|
+
return [...vistos].map(([id, text]) => ({ id, shortDescription: { text } }));
|
|
16111
|
+
}
|
|
16112
|
+
function toSarif(report, version2) {
|
|
16113
|
+
return {
|
|
16114
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
16115
|
+
version: "2.1.0",
|
|
16116
|
+
runs: [
|
|
16117
|
+
{
|
|
16118
|
+
tool: {
|
|
16119
|
+
driver: {
|
|
16120
|
+
name: "VibeCheck",
|
|
16121
|
+
informationUri: "https://vibecheck.kinkai.cloud",
|
|
16122
|
+
version: version2,
|
|
16123
|
+
rules: regras(report.issues)
|
|
16124
|
+
}
|
|
16125
|
+
},
|
|
16126
|
+
results: report.issues.map((issue2) => ({
|
|
16127
|
+
ruleId: issue2.checkId,
|
|
16128
|
+
level: NIVEL[issue2.severity],
|
|
16129
|
+
message: { text: issue2.reason },
|
|
16130
|
+
locations: [{ physicalLocation: { artifactLocation: { uri: issue2.path } } }]
|
|
16131
|
+
}))
|
|
16132
|
+
}
|
|
16133
|
+
]
|
|
16134
|
+
};
|
|
15095
16135
|
}
|
|
15096
16136
|
|
|
15097
16137
|
// src/walker.ts
|
|
15098
|
-
import { lstatSync, readdirSync, readFileSync as
|
|
15099
|
-
import { join as
|
|
16138
|
+
import { lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
16139
|
+
import { join as join6, relative as relative2 } from "path";
|
|
15100
16140
|
var IGNORAR = /* @__PURE__ */ new Set([
|
|
15101
16141
|
"node_modules",
|
|
15102
16142
|
".git",
|
|
@@ -15111,7 +16151,19 @@ var IGNORAR = /* @__PURE__ */ new Set([
|
|
|
15111
16151
|
var CODIGO = /\.(m?[jt]sx?)$/;
|
|
15112
16152
|
var MAX_ARQUIVOS = 800;
|
|
15113
16153
|
var MAX_NOS_WALKER = 120;
|
|
15114
|
-
var
|
|
16154
|
+
var MAX_BYTES_ARQUIVO = 32e3;
|
|
16155
|
+
var EXT = [
|
|
16156
|
+
"",
|
|
16157
|
+
".ts",
|
|
16158
|
+
".tsx",
|
|
16159
|
+
".js",
|
|
16160
|
+
".jsx",
|
|
16161
|
+
".mjs",
|
|
16162
|
+
".cjs",
|
|
16163
|
+
"/index.ts",
|
|
16164
|
+
"/index.tsx",
|
|
16165
|
+
"/index.js"
|
|
16166
|
+
];
|
|
15115
16167
|
var EXPORT_RE = /export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|enum)\s+([A-Za-z_$][\w$]*)/g;
|
|
15116
16168
|
var EXPORT_LISTA_RE = /export\s+\{([^}]+)\}/g;
|
|
15117
16169
|
var FROM_RE = /(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"]([^'"]+)['"]/g;
|
|
@@ -15119,16 +16171,16 @@ var HANDLERS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE",
|
|
|
15119
16171
|
function listar(raiz, dir, acc) {
|
|
15120
16172
|
let nomes;
|
|
15121
16173
|
try {
|
|
15122
|
-
nomes =
|
|
16174
|
+
nomes = readdirSync2(dir);
|
|
15123
16175
|
} catch {
|
|
15124
16176
|
return;
|
|
15125
16177
|
}
|
|
15126
16178
|
for (const nome of nomes) {
|
|
15127
16179
|
if (IGNORAR.has(nome) || nome.startsWith(".")) continue;
|
|
15128
|
-
const cheio =
|
|
16180
|
+
const cheio = join6(dir, nome);
|
|
15129
16181
|
let stat;
|
|
15130
16182
|
try {
|
|
15131
|
-
stat =
|
|
16183
|
+
stat = lstatSync2(cheio);
|
|
15132
16184
|
} catch {
|
|
15133
16185
|
continue;
|
|
15134
16186
|
}
|
|
@@ -15142,7 +16194,7 @@ function listar(raiz, dir, acc) {
|
|
|
15142
16194
|
}
|
|
15143
16195
|
}
|
|
15144
16196
|
function posixRel(raiz, cheio) {
|
|
15145
|
-
return
|
|
16197
|
+
return relative2(raiz, cheio).replaceAll("\\", "/");
|
|
15146
16198
|
}
|
|
15147
16199
|
function dirnamePosix(caminho2) {
|
|
15148
16200
|
const i = caminho2.lastIndexOf("/");
|
|
@@ -15215,7 +16267,7 @@ function resolverSpec(de, spec, indice) {
|
|
|
15215
16267
|
return null;
|
|
15216
16268
|
}
|
|
15217
16269
|
function rotuloDe(rel, exports) {
|
|
15218
|
-
const rota =
|
|
16270
|
+
const rota = routeFromFile(rel);
|
|
15219
16271
|
const handlers = exports.filter((e) => HANDLERS.has(e));
|
|
15220
16272
|
if (rota !== null && handlers.length > 0) return `${handlers[0]} ${rota}`;
|
|
15221
16273
|
if (rota !== null) return rota;
|
|
@@ -15228,7 +16280,7 @@ function lerArquivo(raiz, cheio) {
|
|
|
15228
16280
|
const rel = posixRel(raiz, cheio);
|
|
15229
16281
|
let conteudo = "";
|
|
15230
16282
|
try {
|
|
15231
|
-
conteudo =
|
|
16283
|
+
conteudo = readFileSync4(cheio, "utf8").slice(0, MAX_BYTES_ARQUIVO);
|
|
15232
16284
|
} catch {
|
|
15233
16285
|
return null;
|
|
15234
16286
|
}
|
|
@@ -15236,21 +16288,25 @@ function lerArquivo(raiz, cheio) {
|
|
|
15236
16288
|
const specs = specsDe(conteudo);
|
|
15237
16289
|
return {
|
|
15238
16290
|
rel,
|
|
15239
|
-
|
|
16291
|
+
label: rotuloDe(rel, exports),
|
|
15240
16292
|
exports,
|
|
15241
|
-
importsRel: specs.filter(
|
|
15242
|
-
|
|
16293
|
+
importsRel: specs.filter(
|
|
16294
|
+
(s) => s.startsWith(".") || s.startsWith("@/") || s.startsWith("~/")
|
|
16295
|
+
),
|
|
16296
|
+
importsPkg: specs,
|
|
16297
|
+
data: identifiersFromCode(conteudo)
|
|
15243
16298
|
};
|
|
15244
16299
|
}
|
|
15245
16300
|
function prioridade(info) {
|
|
15246
|
-
const kind =
|
|
16301
|
+
const kind = mapKind({ label: info.label, files: [info.rel] });
|
|
15247
16302
|
if (kind === "api") return 0;
|
|
15248
16303
|
if (kind === "payments" || kind === "auth" || kind === "db") return 1;
|
|
15249
|
-
if (kind === "storage" || kind === "secrets" || kind === "browser" || kind === "ai")
|
|
15250
|
-
|
|
16304
|
+
if (kind === "storage" || kind === "secrets" || kind === "browser" || kind === "ai")
|
|
16305
|
+
return 2;
|
|
16306
|
+
if (info.importsPkg.some((s) => packageId(s) !== null)) return 3;
|
|
15251
16307
|
return 8;
|
|
15252
16308
|
}
|
|
15253
|
-
function
|
|
16309
|
+
function extractFromRepo(raiz) {
|
|
15254
16310
|
const caminhos = [];
|
|
15255
16311
|
listar(raiz, raiz, caminhos);
|
|
15256
16312
|
if (caminhos.length === 0) return null;
|
|
@@ -15265,7 +16321,9 @@ function extrairDoRepo(raiz) {
|
|
|
15265
16321
|
if (infos.length === 0) return null;
|
|
15266
16322
|
const porRel = new Map(infos.map((i) => [i.rel, i]));
|
|
15267
16323
|
const escolhidos = /* @__PURE__ */ new Set();
|
|
15268
|
-
const ordenados = [...infos].sort(
|
|
16324
|
+
const ordenados = [...infos].sort(
|
|
16325
|
+
(a, b) => prioridade(a) - prioridade(b) || a.rel.localeCompare(b.rel)
|
|
16326
|
+
);
|
|
15269
16327
|
for (const info of ordenados) {
|
|
15270
16328
|
if (prioridade(info) <= 3) escolhidos.add(info.rel);
|
|
15271
16329
|
}
|
|
@@ -15295,31 +16353,31 @@ function extrairDoRepo(raiz) {
|
|
|
15295
16353
|
const files = [...escolhidos].map((rel) => porRel.get(rel)).filter((i) => i !== void 0).sort((a, b) => prioridade(a) - prioridade(b) || a.rel.localeCompare(b.rel)).slice(0, MAX_NOS_WALKER);
|
|
15296
16354
|
const nodes = [];
|
|
15297
16355
|
const edges = [];
|
|
15298
|
-
const
|
|
15299
|
-
const
|
|
16356
|
+
const packages = /* @__PURE__ */ new Set();
|
|
16357
|
+
const idDe = /* @__PURE__ */ new Map();
|
|
15300
16358
|
for (const [i, info] of files.entries()) {
|
|
15301
16359
|
const id = `f${String(i)}`;
|
|
15302
|
-
|
|
16360
|
+
idDe.set(info.rel, id);
|
|
15303
16361
|
nodes.push({
|
|
15304
16362
|
id,
|
|
15305
|
-
label: info.
|
|
15306
|
-
identifier: info.exports[0] ?? info.
|
|
16363
|
+
label: info.label,
|
|
16364
|
+
identifier: info.exports[0] ?? info.label,
|
|
15307
16365
|
source_file: info.rel
|
|
15308
16366
|
});
|
|
15309
16367
|
}
|
|
15310
16368
|
for (const info of files) {
|
|
15311
|
-
const de =
|
|
16369
|
+
const de = idDe.get(info.rel);
|
|
15312
16370
|
if (de === void 0) continue;
|
|
15313
16371
|
for (const spec of info.importsRel) {
|
|
15314
16372
|
const alvo = resolverSpec(info.rel, spec, indice);
|
|
15315
|
-
const para = alvo === null ? void 0 :
|
|
16373
|
+
const para = alvo === null ? void 0 : idDe.get(alvo);
|
|
15316
16374
|
if (para === void 0) continue;
|
|
15317
16375
|
edges.push({ source: de, target: para, relation: "imports" });
|
|
15318
16376
|
}
|
|
15319
16377
|
for (const spec of info.importsPkg) {
|
|
15320
|
-
const pkg =
|
|
16378
|
+
const pkg = packageId(spec);
|
|
15321
16379
|
if (pkg === null) continue;
|
|
15322
|
-
|
|
16380
|
+
packages.add(pkg);
|
|
15323
16381
|
const pkgId = `pkg:${pkg}`;
|
|
15324
16382
|
if (!nodes.some((n) => n["id"] === pkgId)) {
|
|
15325
16383
|
nodes.push({ id: pkgId, label: pkg });
|
|
@@ -15327,15 +16385,44 @@ function extrairDoRepo(raiz) {
|
|
|
15327
16385
|
edges.push({ source: de, target: pkgId, relation: "imports" });
|
|
15328
16386
|
}
|
|
15329
16387
|
}
|
|
15330
|
-
const
|
|
15331
|
-
|
|
15332
|
-
|
|
16388
|
+
const data = mergeIdentifiers(
|
|
16389
|
+
infos.map((i) => i.data),
|
|
16390
|
+
{
|
|
16391
|
+
tables: MAX_HINT_TABLES,
|
|
16392
|
+
buckets: MAX_HINT_BUCKETS,
|
|
16393
|
+
collections: MAX_HINT_COLLECTIONS
|
|
16394
|
+
}
|
|
16395
|
+
);
|
|
16396
|
+
const sanitizado = sanitizeGraph({ nodes, edges, packages: [...packages], data });
|
|
16397
|
+
if (!sanitizado.ok || !sanitizado.graph.advanced) return null;
|
|
16398
|
+
return sanitizado.graph;
|
|
15333
16399
|
}
|
|
15334
|
-
function
|
|
15335
|
-
return grafo !== null && grafo.
|
|
16400
|
+
function isAdvancedScan(grafo) {
|
|
16401
|
+
return grafo !== null && grafo.advanced === true;
|
|
15336
16402
|
}
|
|
15337
16403
|
|
|
15338
16404
|
// src/cli.ts
|
|
16405
|
+
var VERSION = "0.2.0";
|
|
16406
|
+
var AJUDA = `vibecheck \u2014 security check for AI-built apps
|
|
16407
|
+
|
|
16408
|
+
npx @kinkai.cloud/vibecheck [options]
|
|
16409
|
+
|
|
16410
|
+
Options
|
|
16411
|
+
--url <url> target; defaults to https://<verified host of the token>
|
|
16412
|
+
--dir <path> repository root (default: current directory)
|
|
16413
|
+
--gate CI mode: HTTP checks + code checks + graph signals,
|
|
16414
|
+
writes ${GATE_DIR}/${GATE_JSON} (+ .sarif) and exits
|
|
16415
|
+
0 passed \xB7 1 failed \xB7 2 inconclusive \xB7 3 error
|
|
16416
|
+
--code-only with --gate: skip the HTTP scan (no quota, no history)
|
|
16417
|
+
--ci never open a browser; requires VIBECHECK_TOKEN
|
|
16418
|
+
--no-wait do not follow the scan; exit as soon as it is queued
|
|
16419
|
+
--gate-consent <list|revoke> manage local code-scan consent
|
|
16420
|
+
--help, --version
|
|
16421
|
+
|
|
16422
|
+
Environment
|
|
16423
|
+
VIBECHECK_TOKEN, VIBECHECK_HOST, VIBECHECK_URL
|
|
16424
|
+
VIBECHECK_ALLOW_CODE_SCAN=1 required by --gate in CI
|
|
16425
|
+
`;
|
|
15339
16426
|
function arg(nome, argv) {
|
|
15340
16427
|
const i = argv.indexOf(nome);
|
|
15341
16428
|
if (i < 0) return null;
|
|
@@ -15347,87 +16434,134 @@ function temFlag(nome, argv) {
|
|
|
15347
16434
|
async function extrairGrafo(raiz) {
|
|
15348
16435
|
const dump = await tentarGraphify(raiz);
|
|
15349
16436
|
if (dump !== null) {
|
|
15350
|
-
const sanitizado =
|
|
15351
|
-
if (sanitizado.ok && sanitizado.
|
|
16437
|
+
const sanitizado = sanitizeGraph(dump);
|
|
16438
|
+
if (sanitizado.ok && sanitizado.graph.advanced) return sanitizado.graph;
|
|
15352
16439
|
}
|
|
15353
|
-
return
|
|
16440
|
+
return extractFromRepo(raiz);
|
|
15354
16441
|
}
|
|
15355
16442
|
async function acompanhar(liveUrl, scanId, shareToken) {
|
|
15356
|
-
const base = urlBase();
|
|
15357
|
-
const streamUrl = `${base}/api/scan/${scanId}/events?t=${encodeURIComponent(shareToken)}`;
|
|
15358
|
-
const resposta = await fetch(streamUrl);
|
|
15359
|
-
if (!resposta.ok || resposta.body === null) {
|
|
15360
|
-
console.warn(`Acompanhe em ${liveUrl}`);
|
|
15361
|
-
return 0;
|
|
15362
|
-
}
|
|
15363
|
-
const leitor = resposta.body.getReader();
|
|
15364
|
-
const decoder = new TextDecoder();
|
|
15365
|
-
let buf = "";
|
|
15366
|
-
let exit = 0;
|
|
15367
16443
|
let gravidades = "";
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
if (done) break;
|
|
15371
|
-
buf += decoder.decode(value, { stream: true });
|
|
15372
|
-
const blocos = buf.split("\n\n");
|
|
15373
|
-
buf = blocos.pop() ?? "";
|
|
15374
|
-
for (const bloco of blocos) {
|
|
15375
|
-
const linha = bloco.split("\n").find((l) => l.startsWith("data: "));
|
|
15376
|
-
if (linha === void 0) continue;
|
|
15377
|
-
let evento;
|
|
15378
|
-
try {
|
|
15379
|
-
evento = JSON.parse(linha.slice(6));
|
|
15380
|
-
} catch {
|
|
15381
|
-
continue;
|
|
15382
|
-
}
|
|
16444
|
+
const resultado = await followScan(urlBase(), scanId, shareToken, {
|
|
16445
|
+
onEvent: (evento) => {
|
|
15383
16446
|
if (evento.type === "finding.found" && (evento.severity === "fatal" || evento.severity === "critical")) {
|
|
15384
|
-
exit = 1;
|
|
15385
16447
|
gravidades = evento.severity;
|
|
15386
16448
|
}
|
|
15387
|
-
if (evento.type === "scan.done" || evento.type === "scan.failed") {
|
|
15388
|
-
if (evento.type === "scan.failed") exit = 1;
|
|
15389
|
-
console.warn(`scan ${evento.type}${gravidades.length > 0 ? ` \xB7 ${gravidades}` : ""}`);
|
|
15390
|
-
return exit;
|
|
15391
|
-
}
|
|
15392
16449
|
}
|
|
16450
|
+
});
|
|
16451
|
+
if (!resultado.opened) {
|
|
16452
|
+
console.warn(`Follow along at ${liveUrl}`);
|
|
16453
|
+
return 0;
|
|
15393
16454
|
}
|
|
15394
|
-
|
|
16455
|
+
if (resultado.final !== null) {
|
|
16456
|
+
console.warn(
|
|
16457
|
+
`scan ${resultado.final.type}${gravidades.length > 0 ? ` \xB7 ${gravidades}` : ""}`
|
|
16458
|
+
);
|
|
16459
|
+
}
|
|
16460
|
+
return resultado.exit;
|
|
16461
|
+
}
|
|
16462
|
+
async function comandoConsentimento(acao, raiz) {
|
|
16463
|
+
if (acao === "list") {
|
|
16464
|
+
const entradas = listarConsentimentos();
|
|
16465
|
+
if (entradas.length === 0) console.warn("no repository consented to code scan");
|
|
16466
|
+
for (const e of entradas) console.warn(`${e.repo} ${e.acceptedAt}`);
|
|
16467
|
+
return 0;
|
|
16468
|
+
}
|
|
16469
|
+
if (acao === "revoke") {
|
|
16470
|
+
console.warn(
|
|
16471
|
+
revogarConsentimento(raiz) ? `revoked for ${raiz}` : `nothing to revoke for ${raiz}`
|
|
16472
|
+
);
|
|
16473
|
+
return 0;
|
|
16474
|
+
}
|
|
16475
|
+
console.error("usage: --gate-consent <list|revoke>");
|
|
16476
|
+
return GATE_EXIT.operational;
|
|
15395
16477
|
}
|
|
15396
16478
|
async function main(argv) {
|
|
16479
|
+
if (temFlag("--help", argv) || temFlag("-h", argv)) {
|
|
16480
|
+
console.warn(AJUDA);
|
|
16481
|
+
return 0;
|
|
16482
|
+
}
|
|
16483
|
+
if (temFlag("--version", argv)) {
|
|
16484
|
+
console.warn(VERSION);
|
|
16485
|
+
return 0;
|
|
16486
|
+
}
|
|
15397
16487
|
const ci = temFlag("--ci", argv) || process.env["CI"] === "true" || process.env["VIBECHECK_TOKEN"] !== void 0 && process.env["VIBECHECK_TOKEN"].length > 0;
|
|
15398
16488
|
const noWait = temFlag("--no-wait", argv);
|
|
15399
16489
|
const raiz = arg("--dir", argv) ?? process.cwd();
|
|
15400
|
-
|
|
15401
|
-
|
|
16490
|
+
const posicional = argv.find(
|
|
16491
|
+
(a, i) => /^https?:\/\//.test(a) && !(argv[i - 1] ?? "").startsWith("--")
|
|
16492
|
+
);
|
|
16493
|
+
let url2 = arg("--url", argv) ?? posicional ?? null;
|
|
16494
|
+
const consentimento = arg("--gate-consent", argv);
|
|
16495
|
+
if (consentimento !== null || temFlag("--gate-consent", argv)) {
|
|
16496
|
+
return comandoConsentimento(consentimento, raiz);
|
|
16497
|
+
}
|
|
16498
|
+
const gate = temFlag("--gate", argv);
|
|
16499
|
+
const codeOnly = gate && temFlag("--code-only", argv);
|
|
16500
|
+
let cred = codeOnly ? { token: "", host: "" } : lerToken();
|
|
15402
16501
|
if (cred === null) {
|
|
15403
16502
|
if (ci) {
|
|
15404
|
-
console.error("CI
|
|
15405
|
-
return 1;
|
|
16503
|
+
console.error("CI requires VIBECHECK_TOKEN");
|
|
16504
|
+
return gate ? GATE_EXIT.operational : 1;
|
|
15406
16505
|
}
|
|
15407
16506
|
cred = await loginDevice(false);
|
|
15408
16507
|
}
|
|
15409
16508
|
if (url2 === null) {
|
|
15410
|
-
if (
|
|
15411
|
-
|
|
15412
|
-
|
|
16509
|
+
if (codeOnly) {
|
|
16510
|
+
url2 = "https://localhost";
|
|
16511
|
+
} else if (cred.host.length === 0) {
|
|
16512
|
+
console.error("pass --url https://your-app.com");
|
|
16513
|
+
return gate ? GATE_EXIT.operational : 1;
|
|
16514
|
+
} else {
|
|
16515
|
+
url2 = `https://${cred.host}`;
|
|
15413
16516
|
}
|
|
15414
|
-
url2 = `https://${cred.host}`;
|
|
15415
16517
|
}
|
|
15416
16518
|
const base = urlBase();
|
|
16519
|
+
if (gate) {
|
|
16520
|
+
const resultado = await runGate(
|
|
16521
|
+
{ dir: raiz, url: url2, token: cred.token, ci, codeOnly },
|
|
16522
|
+
{
|
|
16523
|
+
base,
|
|
16524
|
+
fetch,
|
|
16525
|
+
readRepo: readCodeContext,
|
|
16526
|
+
extractGraph: extrairGrafo,
|
|
16527
|
+
writeArtifact: gravarNoDisco,
|
|
16528
|
+
log: (m) => console.warn(m),
|
|
16529
|
+
now: () => /* @__PURE__ */ new Date()
|
|
16530
|
+
}
|
|
16531
|
+
);
|
|
16532
|
+
if (resultado.report !== null) {
|
|
16533
|
+
escreverArtefatos(raiz, resultado.report, toSarif(resultado.report, VERSION), {
|
|
16534
|
+
writeArtifact: gravarNoDisco
|
|
16535
|
+
});
|
|
16536
|
+
const { status, httpScan, counts } = resultado.report;
|
|
16537
|
+
console.warn(
|
|
16538
|
+
`gate ${status} \xB7 http ${httpScan} \xB7 ${String(counts.fatal + counts.critical + counts.high)} blocking \xB7 ${String(resultado.report.issues.length)} total`
|
|
16539
|
+
);
|
|
16540
|
+
if (resultado.scanId !== void 0) {
|
|
16541
|
+
await enviarGate(resultado.scanId, resultado.report, cred, {
|
|
16542
|
+
base,
|
|
16543
|
+
fetch,
|
|
16544
|
+
log: (m) => console.warn(m)
|
|
16545
|
+
});
|
|
16546
|
+
console.warn(resultado.liveUrl ?? `${base}/report/${resultado.scanId}`);
|
|
16547
|
+
}
|
|
16548
|
+
}
|
|
16549
|
+
return resultado.exit;
|
|
16550
|
+
}
|
|
15417
16551
|
const headers = {
|
|
15418
16552
|
authorization: `Bearer ${cred.token}`,
|
|
15419
16553
|
"content-type": "application/json"
|
|
15420
16554
|
};
|
|
15421
|
-
console.warn("
|
|
16555
|
+
console.warn("Building ScanGraph\u2026");
|
|
15422
16556
|
const grafo = await extrairGrafo(raiz);
|
|
15423
|
-
const
|
|
15424
|
-
if (!
|
|
16557
|
+
const advanced = isAdvancedScan(grafo);
|
|
16558
|
+
if (!advanced) {
|
|
15425
16559
|
console.warn(
|
|
15426
|
-
"
|
|
16560
|
+
"No extractable graph (neither graphify nor JS/TS). HTTP still runs, but this is not an advanced scan."
|
|
15427
16561
|
);
|
|
15428
16562
|
} else {
|
|
15429
16563
|
console.warn(
|
|
15430
|
-
`
|
|
16564
|
+
`ScanGraph ready \xB7 ${String(grafo?.nodes.length ?? 0)} nodes \xB7 ${String(grafo?.edges.length ?? 0)} edges`
|
|
15431
16565
|
);
|
|
15432
16566
|
}
|
|
15433
16567
|
const criado = await fetch(`${base}/api/cli/scan`, {
|
|
@@ -15440,22 +16574,31 @@ async function main(argv) {
|
|
|
15440
16574
|
});
|
|
15441
16575
|
const corpo = await criado.json();
|
|
15442
16576
|
if (!criado.ok || corpo.scanId === void 0) {
|
|
15443
|
-
console.error(corpo.error ?? "
|
|
16577
|
+
console.error(corpo.error ?? "could not open the scan");
|
|
15444
16578
|
return 1;
|
|
15445
16579
|
}
|
|
15446
|
-
if (
|
|
16580
|
+
if (advanced && grafo !== null) {
|
|
15447
16581
|
const put = await fetch(`${base}/api/cli/scan/${corpo.scanId}/graph`, {
|
|
15448
16582
|
method: "PUT",
|
|
15449
16583
|
headers,
|
|
15450
16584
|
body: JSON.stringify(grafo)
|
|
15451
16585
|
});
|
|
15452
|
-
if (!put.ok) console.warn("
|
|
16586
|
+
if (!put.ok) console.warn("graph rejected by the server \u2014 scan continues HTTP-only");
|
|
15453
16587
|
}
|
|
15454
|
-
const live = corpo.liveUrl ?? `${base}/
|
|
16588
|
+
const live = corpo.liveUrl ?? `${base}/advanced/${corpo.scanId}`;
|
|
15455
16589
|
console.warn(live);
|
|
15456
16590
|
if (deveAbrirNavegador({ ci, noWait })) await abrirBrowser(live);
|
|
15457
16591
|
if (noWait) return 0;
|
|
15458
16592
|
return acompanhar(live, corpo.scanId, corpo.shareToken ?? "");
|
|
15459
16593
|
}
|
|
15460
|
-
var codigo
|
|
16594
|
+
var codigo;
|
|
16595
|
+
try {
|
|
16596
|
+
codigo = await main(process.argv.slice(2));
|
|
16597
|
+
} catch (erro) {
|
|
16598
|
+
console.error(erro instanceof Error ? erro.message : String(erro));
|
|
16599
|
+
codigo = GATE_EXIT.operational;
|
|
16600
|
+
}
|
|
15461
16601
|
process.exit(codigo);
|
|
16602
|
+
export {
|
|
16603
|
+
VERSION
|
|
16604
|
+
};
|