@akasecurity/cli 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/dist/cli.js +2008 -1221
- package/package.json +7 -6
- package/web-ui/package.json +1 -1
- package/web-ui/packages/dashboard-ui/package.json +1 -1
- package/web-ui/packages/detections/package.json +2 -2
- package/web-ui/packages/local-ops/package.json +3 -2
- package/web-ui/packages/persistence/package.json +2 -2
- package/web-ui/packages/plugin-sdk/package.json +29 -0
- package/web-ui/packages/schema/package.json +2 -2
- package/web-ui/rules/code-flaws/auth-jwt-no-verify.json +20 -0
- package/web-ui/rules/code-flaws/auth-ssl-verify-false.json +19 -0
- package/web-ui/rules/code-flaws/cmd-inject-exec.json +20 -0
- package/web-ui/rules/code-flaws/cmd-inject-node-exec.json +20 -0
- package/web-ui/rules/code-flaws/cmd-inject-shell.json +20 -0
- package/web-ui/rules/code-flaws/crypto-insecure-random.json +20 -0
- package/web-ui/rules/code-flaws/crypto-weak-hash-md5.json +16 -0
- package/web-ui/rules/code-flaws/crypto-weak-hash-sha1.json +23 -0
- package/web-ui/rules/code-flaws/deser-java-ois.json +22 -0
- package/web-ui/rules/code-flaws/deser-pickle.json +16 -0
- package/web-ui/rules/code-flaws/deser-yaml-unsafe.json +16 -0
- package/web-ui/rules/code-flaws/dev-debug-enabled.json +13 -0
- package/web-ui/rules/code-flaws/dev-placeholder-secret.json +24 -0
- package/web-ui/rules/code-flaws/dev-wildcard-cors.json +17 -0
- package/web-ui/rules/code-flaws/eval-dynamic-exec.json +17 -0
- package/web-ui/rules/code-flaws/hardcoded-password.json +23 -0
- package/web-ui/rules/code-flaws/hardcoded-secret-key.json +17 -0
- package/web-ui/rules/code-flaws/path-traversal-join.json +19 -0
- package/web-ui/rules/code-flaws/path-traversal-open.json +20 -0
- package/web-ui/rules/code-flaws/prototype-pollution-merge.json +16 -0
- package/web-ui/rules/code-flaws/regex-redos-backtrack.json +13 -0
- package/web-ui/rules/code-flaws/sql-inject-concat-dot.json +16 -0
- package/web-ui/rules/code-flaws/sql-inject-concat.json +13 -0
- package/web-ui/rules/code-flaws/sql-inject-format.json +16 -0
- package/web-ui/rules/code-flaws/sql-inject-interp.json +19 -0
- package/web-ui/rules/code-flaws/ssrf-user-url.json +16 -0
- package/web-ui/rules/code-flaws/xss-dangerously-set.json +16 -0
- package/web-ui/rules/code-flaws/xss-inner-html.json +16 -0
- package/web-ui/rules/code-flaws/xss-unescaped-render.json +16 -0
- package/web-ui/rules/core-code-context/db-table-name.json +30 -0
- package/web-ui/rules/core-code-context/feature-flag.json +24 -0
- package/web-ui/rules/core-code-context/file-path.json +17 -0
- package/web-ui/rules/core-code-context/internal-domain.json +13 -0
- package/web-ui/rules/core-code-context/internal-ip.json +13 -0
- package/web-ui/rules/core-code-context/internal-url.json +27 -0
- package/web-ui/rules/core-code-context/localhost-ref.json +13 -0
- package/web-ui/rules/core-code-context/stack-trace.json +13 -0
- package/web-ui/rules/core-financial/credit-card.json +14 -0
- package/web-ui/rules/core-financial/cusip.json +14 -0
- package/web-ui/rules/core-financial/cvv.json +13 -0
- package/web-ui/rules/core-financial/iban.json +17 -0
- package/web-ui/rules/core-financial/paypal.json +14 -0
- package/web-ui/rules/core-financial/routing-number.json +14 -0
- package/web-ui/rules/core-financial/salary.json +23 -0
- package/web-ui/rules/core-financial/swift.json +14 -0
- package/web-ui/rules/core-phi/biometric-ref.json +23 -0
- package/web-ui/rules/core-phi/genetic-data.json +24 -0
- package/web-ui/rules/core-phi/group-id.json +19 -0
- package/web-ui/rules/core-phi/hipaa-identifier.json +13 -0
- package/web-ui/rules/core-phi/icd10-code.json +14 -0
- package/web-ui/rules/core-phi/member-id.json +13 -0
- package/web-ui/rules/core-phi/mrn.json +13 -0
- package/web-ui/rules/core-phi/ndc-code.json +13 -0
- package/web-ui/rules/core-pii/dob.json +18 -0
- package/web-ui/rules/core-pii/drivers-license-us.json +14 -0
- package/web-ui/rules/core-pii/email.json +13 -0
- package/web-ui/rules/core-pii/home-address.json +21 -0
- package/web-ui/rules/core-pii/ip-address.json +13 -0
- package/web-ui/rules/core-pii/mac-address.json +13 -0
- package/web-ui/rules/core-pii/name.json +21 -0
- package/web-ui/rules/core-pii/national-id.json +27 -0
- package/web-ui/rules/core-pii/passport-us.json +17 -0
- package/web-ui/rules/core-pii/phone-intl.json +13 -0
- package/web-ui/rules/core-pii/phone-us.json +17 -0
- package/web-ui/rules/core-pii/ssn.json +13 -0
- package/web-ui/rules/core-pii/vin.json +13 -0
- package/web-ui/rules/core-pii/zip.json +30 -0
- package/web-ui/rules/secrets/anthropic-api-key.json +14 -0
- package/web-ui/rules/secrets/aws-access-key.json +14 -0
- package/web-ui/rules/secrets/aws-secret-key.json +15 -0
- package/web-ui/rules/secrets/azure-connection-string.json +13 -0
- package/web-ui/rules/secrets/cloudflare-api-key.json +15 -0
- package/web-ui/rules/secrets/datadog-key.json +15 -0
- package/web-ui/rules/secrets/digitalocean-token.json +13 -0
- package/web-ui/rules/secrets/discord-token.json +14 -0
- package/web-ui/rules/secrets/gcp-service-account.json +16 -0
- package/web-ui/rules/secrets/github-pat.json +16 -0
- package/web-ui/rules/secrets/gitlab-token.json +14 -0
- package/web-ui/rules/secrets/heroku-api-key.json +14 -0
- package/web-ui/rules/secrets/npm-token.json +14 -0
- package/web-ui/rules/secrets/openai-api-key.json +14 -0
- package/web-ui/rules/secrets/pulumi-access-token.json +14 -0
- package/web-ui/rules/secrets/sendgrid-key.json +14 -0
- package/web-ui/rules/secrets/slack-token.json +13 -0
- package/web-ui/rules/secrets/stripe-live-key.json +14 -0
- package/web-ui/rules/secrets/terraform-cloud-token.json +16 -0
- package/web-ui/rules/secrets/twilio-key.json +13 -0
- package/web-ui/rules/secrets/vault-token.json +14 -0
- package/web-ui/rules/secrets-infra/api-key-header.json +15 -0
- package/web-ui/rules/secrets-infra/basic-auth-header.json +13 -0
- package/web-ui/rules/secrets-infra/basic-auth-url.json +13 -0
- package/web-ui/rules/secrets-infra/bearer-token.json +17 -0
- package/web-ui/rules/secrets-infra/db-connection-string.json +13 -0
- package/web-ui/rules/secrets-infra/docker-config-auth.json +13 -0
- package/web-ui/rules/secrets-infra/env-key-value.json +14 -0
- package/web-ui/rules/secrets-infra/generic-high-entropy-secret.json +18 -0
- package/web-ui/rules/secrets-infra/jwt-token.json +16 -0
- package/web-ui/rules/secrets-infra/kubeconfig-token.json +14 -0
- package/web-ui/rules/secrets-infra/password-field.json +14 -0
- package/web-ui/rules/secrets-infra/pgp-private-key.json +15 -0
- package/web-ui/rules/secrets-infra/ssh-private-key.json +15 -0
- package/web-ui/web-ui/.next/BUILD_ID +1 -1
- package/web-ui/web-ui/.next/app-build-manifest.json +39 -39
- package/web-ui/web-ui/.next/app-path-routes-manifest.json +6 -6
- package/web-ui/web-ui/.next/build-manifest.json +2 -2
- package/web-ui/web-ui/.next/prerender-manifest.json +3 -3
- package/web-ui/web-ui/.next/required-server-files.json +2 -1
- package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found.rsc +2 -2
- package/web-ui/web-ui/.next/server/app/index.html +1 -1
- package/web-ui/web-ui/.next/server/app/index.rsc +2 -2
- package/web-ui/web-ui/.next/server/app/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app-paths-manifest.json +6 -6
- package/web-ui/web-ui/.next/server/chunks/366.js +566 -0
- package/web-ui/web-ui/.next/server/chunks/45.js +1 -1
- package/web-ui/web-ui/.next/server/chunks/484.js +564 -0
- package/web-ui/web-ui/.next/server/chunks/913.js +2 -0
- package/web-ui/web-ui/.next/server/functions-config-manifest.json +7 -7
- package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
- package/web-ui/web-ui/.next/server/pages/404.html +1 -1
- package/web-ui/web-ui/.next/server/pages/500.html +1 -1
- package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
- package/web-ui/web-ui/.next/static/chunks/561-46ea4b9ad0419130.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/{page-f6b1b344709ee48e.js → page-49c63d0cec0da80d.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/{page-485a76552d6dbabd.js → page-d469bd704615ea76.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07ca4b0ce174ef7a.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-16d5fd7d373c8f0b.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/{page-e8498a1e48943abf.js → page-d6cfd847bb528eec.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-291617f3a4d46a01.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/{page-94eb9d838ae00222.js → page-c59135e0cfa8d5c7.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-0eb54f5acb30a73e.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-b00573441ac43bdf.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/{page-b9fba600c590896d.js → page-3b5fe92cd995b763.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-50f828314c9a5d91.js +1 -0
- package/web-ui/web-ui/.next/static/css/dfdcbdce6b7c9eb0.css +2 -0
- package/web-ui/web-ui/package.json +1 -1
- package/web-ui/web-ui/server.js +1 -1
- package/LICENSE +0 -201
- package/web-ui/web-ui/.next/server/chunks/116.js +0 -536
- package/web-ui/web-ui/.next/server/chunks/223.js +0 -534
- package/web-ui/web-ui/.next/server/chunks/226.js +0 -2
- package/web-ui/web-ui/.next/static/chunks/561-3a19cd28d2b941fa.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-8030433ea5c099fa.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-452be710e9e05ad5.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-1b841b6a60708c6d.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-dd61001da0f891d3.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-158ed3ad2cee998b.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-241fdcbac6e942aa.js +0 -1
- package/web-ui/web-ui/.next/static/css/69dfaec5234c6b99.css +0 -2
- /package/web-ui/web-ui/.next/static/{v339u67-B5AsSrzDBfv22 → zNaHfOWePiRki0ugHKYzl}/_buildManifest.js +0 -0
- /package/web-ui/web-ui/.next/static/{v339u67-B5AsSrzDBfv22 → zNaHfOWePiRki0ugHKYzl}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -554,10 +554,10 @@ var AGENT_PLUGINS = [
|
|
|
554
554
|
name: "Claude Code",
|
|
555
555
|
sourceTool: "claude-code",
|
|
556
556
|
description: "Hooks Claude Code sessions to detect + redact sensitive data in prompts, responses, and file writes.",
|
|
557
|
-
npmPackage: "@akasecurity/
|
|
558
|
-
pluginName: "
|
|
559
|
-
marketplace: "
|
|
560
|
-
marketplaceSource: "akasecurity/
|
|
557
|
+
npmPackage: "@akasecurity/ai-tc-claude-code",
|
|
558
|
+
pluginName: "ai-tc",
|
|
559
|
+
marketplace: "akasecurity",
|
|
560
|
+
marketplaceSource: "akasecurity/marketplace"
|
|
561
561
|
}
|
|
562
562
|
];
|
|
563
563
|
function findAgent(id) {
|
|
@@ -16109,6 +16109,11 @@ var AuditEventType = external_exports.enum([
|
|
|
16109
16109
|
"prompt",
|
|
16110
16110
|
"response",
|
|
16111
16111
|
"code_change",
|
|
16112
|
+
// The events.kind of a scanned tool call, widened in to keep this a
|
|
16113
|
+
// superset. Narrower than 'tool_call' above and not a duplicate of it:
|
|
16114
|
+
// 'tool_call' is the reconciler's structural row for every call, while
|
|
16115
|
+
// 'tool_use' exists only where a hook enforced against the arguments.
|
|
16116
|
+
"tool_use",
|
|
16112
16117
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
16113
16118
|
// fact the posture inspection findings reference (findings require an
|
|
16114
16119
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -16500,7 +16505,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
16500
16505
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
16501
16506
|
|
|
16502
16507
|
// ../packages/schema/src/zod/event.ts
|
|
16503
|
-
var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
|
|
16508
|
+
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
16504
16509
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
16505
16510
|
var EventMetadata = external_exports.object({
|
|
16506
16511
|
sessionId: external_exports.string().optional(),
|
|
@@ -16813,7 +16818,7 @@ var DetectionException = external_exports.object({
|
|
|
16813
16818
|
justification: external_exports.string().min(1),
|
|
16814
16819
|
conditions: ExceptionConditions.nullable(),
|
|
16815
16820
|
createdBy: external_exports.string(),
|
|
16816
|
-
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
|
|
16821
|
+
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
|
|
16817
16822
|
createdAt: external_exports.iso.datetime(),
|
|
16818
16823
|
updatedAt: external_exports.iso.datetime(),
|
|
16819
16824
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16982,20 +16987,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16982
16987
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16983
16988
|
fetchedAt: external_exports.iso.datetime()
|
|
16984
16989
|
}).meta({ id: "PolicyBundle" });
|
|
16985
|
-
var DEFAULT_ACTIONS = {
|
|
16986
|
-
secret: "block",
|
|
16987
|
-
pii: "redact",
|
|
16988
|
-
financial: "redact",
|
|
16989
|
-
phi: "redact",
|
|
16990
|
-
code_context: "warn",
|
|
16991
|
-
code_flaw: "warn",
|
|
16992
|
-
custom: "warn",
|
|
16993
|
-
// Config-posture findings only observe today (they land in
|
|
16994
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16995
|
-
config: "warn"
|
|
16996
|
-
};
|
|
16997
16990
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16998
16991
|
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
16992
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
16993
|
+
secret: "critical",
|
|
16994
|
+
financial: "critical",
|
|
16995
|
+
// core-financial/credit-card
|
|
16996
|
+
code_flaw: "critical",
|
|
16997
|
+
pii: "high",
|
|
16998
|
+
phi: "high",
|
|
16999
|
+
custom: "high",
|
|
17000
|
+
// user-defined; conservative
|
|
17001
|
+
code_context: "low",
|
|
17002
|
+
config: "low"
|
|
17003
|
+
// observe-only; floors to monitor regardless
|
|
17004
|
+
};
|
|
17005
|
+
function severityFloorPolicy(category) {
|
|
17006
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
17007
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
17008
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
17009
|
+
}
|
|
16999
17010
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
17000
17011
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
17001
17012
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -17022,6 +17033,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
17022
17033
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
17023
17034
|
}
|
|
17024
17035
|
};
|
|
17036
|
+
function builtinPolicyToAction(id) {
|
|
17037
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
17038
|
+
}
|
|
17039
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
17040
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
17041
|
+
);
|
|
17025
17042
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
17026
17043
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
17027
17044
|
);
|
|
@@ -17428,10 +17445,8 @@ function toApiProvider(sourceTool) {
|
|
|
17428
17445
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
17429
17446
|
}
|
|
17430
17447
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
17431
|
-
function
|
|
17432
|
-
const statuses = new Set(
|
|
17433
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
17434
|
-
);
|
|
17448
|
+
function foldGroupStatus(instanceStatuses) {
|
|
17449
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
17435
17450
|
if (statuses.size === 0) return void 0;
|
|
17436
17451
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
17437
17452
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -17449,6 +17464,7 @@ function deriveFindingStatus(row) {
|
|
|
17449
17464
|
function buildFindingGroups(rows, opts = {}) {
|
|
17450
17465
|
const overrides = opts.overrides;
|
|
17451
17466
|
const packNames = opts.packNames;
|
|
17467
|
+
const aggregates = opts.aggregates;
|
|
17452
17468
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
17453
17469
|
for (const row of rows) {
|
|
17454
17470
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -17470,17 +17486,20 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
17470
17486
|
status: r.status
|
|
17471
17487
|
};
|
|
17472
17488
|
});
|
|
17473
|
-
const
|
|
17489
|
+
const agg = aggregates?.get(ruleId);
|
|
17490
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
17474
17491
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
17475
17492
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
17476
17493
|
);
|
|
17477
17494
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
17478
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
17495
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
17479
17496
|
if (seenProviders.has(p)) return false;
|
|
17480
17497
|
seenProviders.add(p);
|
|
17481
17498
|
return true;
|
|
17482
17499
|
});
|
|
17483
|
-
const actionSet = new Set(
|
|
17500
|
+
const actionSet = new Set(
|
|
17501
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
17502
|
+
);
|
|
17484
17503
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
17485
17504
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
17486
17505
|
const detection = {
|
|
@@ -17494,8 +17513,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
17494
17513
|
contextPrefix: ""
|
|
17495
17514
|
// empty (pending privacy review)
|
|
17496
17515
|
};
|
|
17497
|
-
const status =
|
|
17498
|
-
|
|
17516
|
+
const status = foldGroupStatus(
|
|
17517
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
17518
|
+
);
|
|
17519
|
+
const group = {
|
|
17499
17520
|
id: ruleId,
|
|
17500
17521
|
category: apiCategory,
|
|
17501
17522
|
subtype: ruleId,
|
|
@@ -17504,21 +17525,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
17504
17525
|
match,
|
|
17505
17526
|
detection,
|
|
17506
17527
|
policy,
|
|
17507
|
-
instanceCount: instances.length,
|
|
17528
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
17508
17529
|
providers,
|
|
17509
17530
|
aggregateAction,
|
|
17510
17531
|
latestDetectedAt,
|
|
17511
17532
|
instances,
|
|
17512
17533
|
status
|
|
17513
|
-
}
|
|
17534
|
+
};
|
|
17535
|
+
if (agg) {
|
|
17536
|
+
actionsCache.set(group, [...actionSet]);
|
|
17537
|
+
if (agg.searchText !== void 0) {
|
|
17538
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
17539
|
+
}
|
|
17540
|
+
}
|
|
17541
|
+
groups.push(group);
|
|
17514
17542
|
}
|
|
17515
17543
|
return groups;
|
|
17516
17544
|
}
|
|
17517
17545
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
17518
|
-
function
|
|
17519
|
-
|
|
17520
|
-
if (cached2 !== void 0) return cached2;
|
|
17521
|
-
const haystack = [
|
|
17546
|
+
function buildHaystack(g, extra) {
|
|
17547
|
+
return [
|
|
17522
17548
|
g.subtype,
|
|
17523
17549
|
g.category,
|
|
17524
17550
|
g.match.maskedValue,
|
|
@@ -17526,11 +17552,25 @@ function groupHaystack(g) {
|
|
|
17526
17552
|
g.id,
|
|
17527
17553
|
...g.instances.map((i) => i.repo),
|
|
17528
17554
|
...g.instances.map((i) => i.file),
|
|
17529
|
-
...g.instances.map((i) => i.id)
|
|
17555
|
+
...g.instances.map((i) => i.id),
|
|
17556
|
+
...extra === void 0 ? [] : [extra]
|
|
17530
17557
|
].join(" ").toLowerCase();
|
|
17558
|
+
}
|
|
17559
|
+
function groupHaystack(g) {
|
|
17560
|
+
const cached2 = haystackCache.get(g);
|
|
17561
|
+
if (cached2 !== void 0) return cached2;
|
|
17562
|
+
const haystack = buildHaystack(g);
|
|
17531
17563
|
haystackCache.set(g, haystack);
|
|
17532
17564
|
return haystack;
|
|
17533
17565
|
}
|
|
17566
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
17567
|
+
function groupActions(g) {
|
|
17568
|
+
const cached2 = actionsCache.get(g);
|
|
17569
|
+
if (cached2 !== void 0) return cached2;
|
|
17570
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
17571
|
+
actionsCache.set(g, actions);
|
|
17572
|
+
return actions;
|
|
17573
|
+
}
|
|
17534
17574
|
function applyFindingFilters(groups, opts) {
|
|
17535
17575
|
let filtered = groups;
|
|
17536
17576
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17543,7 +17583,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
17543
17583
|
}
|
|
17544
17584
|
if (opts.actions && opts.actions.length > 0) {
|
|
17545
17585
|
const actionSet = new Set(opts.actions);
|
|
17546
|
-
filtered = filtered.filter((g) => g.
|
|
17586
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
17547
17587
|
}
|
|
17548
17588
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
17549
17589
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -17595,8 +17635,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17595
17635
|
});
|
|
17596
17636
|
const actionMap = /* @__PURE__ */ new Map();
|
|
17597
17637
|
for (const g of forAction) {
|
|
17598
|
-
const
|
|
17599
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17638
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17600
17639
|
}
|
|
17601
17640
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17602
17641
|
providers: opts.providers,
|
|
@@ -18158,6 +18197,36 @@ function reviewSeverityRank(reasons) {
|
|
|
18158
18197
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
18159
18198
|
}
|
|
18160
18199
|
|
|
18200
|
+
// ../packages/schema/src/zod/triage.ts
|
|
18201
|
+
var TriageHit = external_exports.object({
|
|
18202
|
+
ruleId: external_exports.string(),
|
|
18203
|
+
category: DetectionCategory,
|
|
18204
|
+
severity: Severity,
|
|
18205
|
+
maskedMatch: external_exports.string(),
|
|
18206
|
+
rawMatch: external_exports.string(),
|
|
18207
|
+
context: external_exports.string(),
|
|
18208
|
+
filePath: external_exports.string().optional(),
|
|
18209
|
+
confidence: external_exports.number().min(0).max(1),
|
|
18210
|
+
id: external_exports.string().optional(),
|
|
18211
|
+
valueFingerprint: external_exports.string().optional(),
|
|
18212
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
18213
|
+
});
|
|
18214
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
18215
|
+
var TriageCategoryRec = external_exports.object({
|
|
18216
|
+
category: DetectionCategory,
|
|
18217
|
+
action: TriagePolicy,
|
|
18218
|
+
reasoning: external_exports.string(),
|
|
18219
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
18220
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
18221
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
18222
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
18223
|
+
fpIds: external_exports.array(external_exports.string())
|
|
18224
|
+
});
|
|
18225
|
+
var TriageRecommendation = external_exports.object({
|
|
18226
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
18227
|
+
notes: external_exports.string()
|
|
18228
|
+
});
|
|
18229
|
+
|
|
18161
18230
|
// ../packages/local-ops/src/fs-scan.ts
|
|
18162
18231
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
18163
18232
|
var AKAIGNORE_FILENAME = ".akaignore";
|
|
@@ -18281,10 +18350,9 @@ function scanPathIntoStore(db, target, opts = {}) {
|
|
|
18281
18350
|
return { scanned, findings: findingCount, files };
|
|
18282
18351
|
}
|
|
18283
18352
|
|
|
18284
|
-
// ../packages/local-ops/src/
|
|
18285
|
-
import {
|
|
18286
|
-
import {
|
|
18287
|
-
import { join as join7 } from "path";
|
|
18353
|
+
// ../packages/local-ops/src/project-inventory.ts
|
|
18354
|
+
import { statSync as statSync5 } from "fs";
|
|
18355
|
+
import { resolve } from "path";
|
|
18288
18356
|
|
|
18289
18357
|
// ../packages/persistence/src/database.ts
|
|
18290
18358
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
@@ -18292,31 +18360,69 @@ import { existsSync as existsSync2, renameSync, rmSync } from "fs";
|
|
|
18292
18360
|
import { join as join3, sep as sep2 } from "path";
|
|
18293
18361
|
import { DatabaseSync } from "node:sqlite";
|
|
18294
18362
|
|
|
18295
|
-
// ../packages/persistence/src/
|
|
18296
|
-
|
|
18297
|
-
|
|
18298
|
-
return createHash2("sha256").update(input).digest("hex");
|
|
18363
|
+
// ../packages/persistence/src/internal/sql-text.ts
|
|
18364
|
+
function escapeLikePattern(s) {
|
|
18365
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
18299
18366
|
}
|
|
18300
|
-
function
|
|
18301
|
-
return
|
|
18367
|
+
function placeholders(n) {
|
|
18368
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
18302
18369
|
}
|
|
18303
|
-
function
|
|
18304
|
-
return
|
|
18370
|
+
function containsPattern(q) {
|
|
18371
|
+
return `%${escapeLikePattern(q)}%`;
|
|
18305
18372
|
}
|
|
18306
|
-
function
|
|
18307
|
-
return
|
|
18373
|
+
function likeAny(exprs) {
|
|
18374
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
18308
18375
|
}
|
|
18309
|
-
|
|
18310
|
-
|
|
18376
|
+
|
|
18377
|
+
// ../packages/persistence/src/internal/transactions.ts
|
|
18378
|
+
var savepointSeq = 0;
|
|
18379
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
18380
|
+
if (db.isTransaction) {
|
|
18381
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
18382
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
18383
|
+
try {
|
|
18384
|
+
fn();
|
|
18385
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
18386
|
+
} catch (error51) {
|
|
18387
|
+
try {
|
|
18388
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
18389
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
18390
|
+
} catch {
|
|
18391
|
+
}
|
|
18392
|
+
throw error51;
|
|
18393
|
+
}
|
|
18394
|
+
return;
|
|
18395
|
+
}
|
|
18396
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
18397
|
+
try {
|
|
18398
|
+
fn();
|
|
18399
|
+
db.exec("COMMIT");
|
|
18400
|
+
} catch (error51) {
|
|
18401
|
+
try {
|
|
18402
|
+
db.exec("ROLLBACK");
|
|
18403
|
+
} catch {
|
|
18404
|
+
}
|
|
18405
|
+
throw error51;
|
|
18406
|
+
}
|
|
18311
18407
|
}
|
|
18312
|
-
function
|
|
18313
|
-
|
|
18408
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
18409
|
+
const nested = db.isTransaction;
|
|
18410
|
+
try {
|
|
18411
|
+
withTransaction(db, fn, mode);
|
|
18412
|
+
return true;
|
|
18413
|
+
} catch (error51) {
|
|
18414
|
+
if (!db.isTransaction && nested) throw error51;
|
|
18415
|
+
return false;
|
|
18416
|
+
}
|
|
18314
18417
|
}
|
|
18315
|
-
|
|
18316
|
-
|
|
18418
|
+
|
|
18419
|
+
// ../packages/persistence/src/internal/warn.ts
|
|
18420
|
+
function akaWarn(message) {
|
|
18421
|
+
process.stderr.write(`[aka] ${message}
|
|
18422
|
+
`);
|
|
18317
18423
|
}
|
|
18318
18424
|
|
|
18319
|
-
// ../packages/persistence/src/migrations.ts
|
|
18425
|
+
// ../packages/persistence/src/db/migrations/introspection.ts
|
|
18320
18426
|
function evidenceObjects(sql) {
|
|
18321
18427
|
const objects = [];
|
|
18322
18428
|
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
@@ -18331,14 +18437,50 @@ function evidenceObjects(sql) {
|
|
|
18331
18437
|
}
|
|
18332
18438
|
return objects;
|
|
18333
18439
|
}
|
|
18440
|
+
function schemaObjectExists(db, kind, name) {
|
|
18441
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
18442
|
+
return row !== void 0;
|
|
18443
|
+
}
|
|
18444
|
+
function indexExists(db, name) {
|
|
18445
|
+
return schemaObjectExists(db, "index", name);
|
|
18446
|
+
}
|
|
18447
|
+
function columnNames(db, table, opts) {
|
|
18448
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
18449
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
|
|
18450
|
+
return columns.map((c) => c.name);
|
|
18451
|
+
}
|
|
18334
18452
|
function evidenceExists(db, object2) {
|
|
18335
18453
|
if (object2.kind === "column") {
|
|
18336
|
-
|
|
18337
|
-
return columns.some((c) => c.name === object2.name);
|
|
18454
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
18338
18455
|
}
|
|
18339
|
-
|
|
18340
|
-
|
|
18456
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
18457
|
+
}
|
|
18458
|
+
|
|
18459
|
+
// ../packages/persistence/src/ids.ts
|
|
18460
|
+
import { createHash as createHash2 } from "crypto";
|
|
18461
|
+
function sha256Hex(input) {
|
|
18462
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
18463
|
+
}
|
|
18464
|
+
function inventoryId(objectType, identityKey) {
|
|
18465
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
18466
|
+
}
|
|
18467
|
+
function sourceProjectId(url2) {
|
|
18468
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
18469
|
+
}
|
|
18470
|
+
function classifiedDataId(cls) {
|
|
18471
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
18472
|
+
}
|
|
18473
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
18474
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
18341
18475
|
}
|
|
18476
|
+
function llmCallId(sessionId, messageId) {
|
|
18477
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
18478
|
+
}
|
|
18479
|
+
function toolCallId(sessionId, toolUseId) {
|
|
18480
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
18481
|
+
}
|
|
18482
|
+
|
|
18483
|
+
// ../packages/persistence/src/migrations.ts
|
|
18342
18484
|
function describeObject(object2) {
|
|
18343
18485
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
18344
18486
|
}
|
|
@@ -18349,10 +18491,6 @@ function createdIndexName(statement) {
|
|
|
18349
18491
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
18350
18492
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
18351
18493
|
}
|
|
18352
|
-
function indexExists(db, name) {
|
|
18353
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
18354
|
-
return row !== void 0;
|
|
18355
|
-
}
|
|
18356
18494
|
function applyMigrations(db) {
|
|
18357
18495
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
18358
18496
|
db.exec(
|
|
@@ -18371,44 +18509,39 @@ function applyMigrations(db) {
|
|
|
18371
18509
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
18372
18510
|
if (present.length > 0 && present.length < evidence.length) {
|
|
18373
18511
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
18374
|
-
const message = `
|
|
18375
|
-
|
|
18376
|
-
`);
|
|
18377
|
-
throw new Error(message);
|
|
18512
|
+
const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
|
|
18513
|
+
akaWarn(message);
|
|
18514
|
+
throw new Error(`[aka] ${message}`);
|
|
18378
18515
|
}
|
|
18379
18516
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
18380
18517
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
18381
18518
|
const statements = splitStatements(migration.sql);
|
|
18382
18519
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
18383
18520
|
try {
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
const
|
|
18388
|
-
|
|
18389
|
-
if (
|
|
18390
|
-
|
|
18391
|
-
|
|
18521
|
+
withTransaction(
|
|
18522
|
+
db,
|
|
18523
|
+
() => {
|
|
18524
|
+
for (const statement of statements) {
|
|
18525
|
+
const indexName = createdIndexName(statement);
|
|
18526
|
+
if (indexName === void 0) {
|
|
18527
|
+
if (alreadyApplied) continue;
|
|
18528
|
+
} else if (indexExists(db, indexName)) {
|
|
18529
|
+
continue;
|
|
18530
|
+
}
|
|
18531
|
+
db.exec(statement);
|
|
18392
18532
|
}
|
|
18393
|
-
|
|
18394
|
-
|
|
18395
|
-
|
|
18396
|
-
|
|
18397
|
-
|
|
18398
|
-
|
|
18399
|
-
|
|
18400
|
-
);
|
|
18533
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
18534
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
18535
|
+
if (violations.length > 0) {
|
|
18536
|
+
throw new Error(
|
|
18537
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
18538
|
+
);
|
|
18539
|
+
}
|
|
18401
18540
|
}
|
|
18402
|
-
|
|
18403
|
-
|
|
18404
|
-
|
|
18405
|
-
|
|
18406
|
-
try {
|
|
18407
|
-
db.exec("ROLLBACK");
|
|
18408
|
-
} catch {
|
|
18409
|
-
}
|
|
18410
|
-
throw error51;
|
|
18411
|
-
}
|
|
18541
|
+
record2.run(migration.tag, Date.now());
|
|
18542
|
+
},
|
|
18543
|
+
"IMMEDIATE"
|
|
18544
|
+
);
|
|
18412
18545
|
} finally {
|
|
18413
18546
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
18414
18547
|
}
|
|
@@ -18451,8 +18584,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18451
18584
|
}
|
|
18452
18585
|
];
|
|
18453
18586
|
function ensureTokenUsageColumns(db) {
|
|
18454
|
-
const
|
|
18455
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
18587
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18456
18588
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18457
18589
|
if (!existing.has(column.name)) {
|
|
18458
18590
|
db.exec(column.ddl);
|
|
@@ -18490,47 +18622,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
18490
18622
|
repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
|
|
18491
18623
|
}));
|
|
18492
18624
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
18493
|
-
|
|
18494
|
-
|
|
18495
|
-
|
|
18496
|
-
|
|
18497
|
-
|
|
18498
|
-
|
|
18499
|
-
|
|
18500
|
-
|
|
18501
|
-
|
|
18502
|
-
|
|
18503
|
-
|
|
18504
|
-
|
|
18505
|
-
|
|
18506
|
-
dropCollisions
|
|
18507
|
-
|
|
18625
|
+
withTransaction(
|
|
18626
|
+
db,
|
|
18627
|
+
() => {
|
|
18628
|
+
for (const { row, canonicalId } of legacy) {
|
|
18629
|
+
foldProject.run(
|
|
18630
|
+
canonicalId,
|
|
18631
|
+
row.url,
|
|
18632
|
+
row.name,
|
|
18633
|
+
row.attributes,
|
|
18634
|
+
row.firstSeen,
|
|
18635
|
+
row.lastSeen
|
|
18636
|
+
);
|
|
18637
|
+
repointAudit.run(canonicalId, row.id);
|
|
18638
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
18639
|
+
dropCollisions.run(row.id, canonicalId);
|
|
18640
|
+
repoint.run(canonicalId, row.id);
|
|
18641
|
+
}
|
|
18642
|
+
repointCallSite.run(canonicalId, row.id);
|
|
18643
|
+
deleteLegacy.run(row.id);
|
|
18508
18644
|
}
|
|
18509
|
-
|
|
18510
|
-
|
|
18511
|
-
|
|
18512
|
-
db.exec("COMMIT");
|
|
18513
|
-
} catch (error51) {
|
|
18514
|
-
try {
|
|
18515
|
-
db.exec("ROLLBACK");
|
|
18516
|
-
} catch {
|
|
18517
|
-
}
|
|
18518
|
-
throw error51;
|
|
18519
|
-
}
|
|
18645
|
+
},
|
|
18646
|
+
"IMMEDIATE"
|
|
18647
|
+
);
|
|
18520
18648
|
} catch (error51) {
|
|
18521
|
-
|
|
18522
|
-
`);
|
|
18649
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18523
18650
|
}
|
|
18524
18651
|
}
|
|
18525
18652
|
function isForeignSqliteLineage(db) {
|
|
18526
|
-
|
|
18527
|
-
|
|
18528
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
18529
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
18653
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18654
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
18530
18655
|
}
|
|
18531
18656
|
function ensureSyncedAtColumn(db, table) {
|
|
18532
|
-
|
|
18533
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
18657
|
+
if (!columnNames(db, table).includes("synced_at")) {
|
|
18534
18658
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
18535
18659
|
}
|
|
18536
18660
|
}
|
|
@@ -18581,8 +18705,11 @@ function ensureDataDirSync(dir) {
|
|
|
18581
18705
|
} catch {
|
|
18582
18706
|
}
|
|
18583
18707
|
}
|
|
18708
|
+
function walSidecars(file2) {
|
|
18709
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
18710
|
+
}
|
|
18584
18711
|
function tightenPerms(file2) {
|
|
18585
|
-
for (const path of [file2,
|
|
18712
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18586
18713
|
try {
|
|
18587
18714
|
chmodSync(path, DATA_FILE_MODE);
|
|
18588
18715
|
} catch {
|
|
@@ -18590,12 +18717,68 @@ function tightenPerms(file2) {
|
|
|
18590
18717
|
}
|
|
18591
18718
|
}
|
|
18592
18719
|
|
|
18593
|
-
// ../packages/persistence/src/
|
|
18594
|
-
function
|
|
18595
|
-
|
|
18720
|
+
// ../packages/persistence/src/internal/json.ts
|
|
18721
|
+
function safeJson(s, fallback) {
|
|
18722
|
+
if (s == null) return fallback;
|
|
18723
|
+
try {
|
|
18724
|
+
return JSON.parse(s);
|
|
18725
|
+
} catch {
|
|
18726
|
+
return fallback;
|
|
18727
|
+
}
|
|
18596
18728
|
}
|
|
18597
|
-
function
|
|
18598
|
-
|
|
18729
|
+
function parseJsonObject(s) {
|
|
18730
|
+
if (s == null) return void 0;
|
|
18731
|
+
try {
|
|
18732
|
+
const parsed = JSON.parse(s);
|
|
18733
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18734
|
+
} catch {
|
|
18735
|
+
}
|
|
18736
|
+
return void 0;
|
|
18737
|
+
}
|
|
18738
|
+
|
|
18739
|
+
// ../packages/persistence/src/internal/rows.ts
|
|
18740
|
+
function allRows(stmt, params) {
|
|
18741
|
+
if (params === void 0) return stmt.all();
|
|
18742
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18743
|
+
return stmt.all(params);
|
|
18744
|
+
}
|
|
18745
|
+
function getRow(stmt, params) {
|
|
18746
|
+
if (params === void 0) return stmt.get();
|
|
18747
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18748
|
+
return stmt.get(params);
|
|
18749
|
+
}
|
|
18750
|
+
function intToBool(raw) {
|
|
18751
|
+
return raw === 1 || raw === true;
|
|
18752
|
+
}
|
|
18753
|
+
function boolToInt(b) {
|
|
18754
|
+
return b ? 1 : 0;
|
|
18755
|
+
}
|
|
18756
|
+
function bindParams(row) {
|
|
18757
|
+
const out = {};
|
|
18758
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18759
|
+
out[key] = value === void 0 ? null : value;
|
|
18760
|
+
}
|
|
18761
|
+
return out;
|
|
18762
|
+
}
|
|
18763
|
+
function countScalar(db, sql, params) {
|
|
18764
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18765
|
+
}
|
|
18766
|
+
function countBy(db, sql, params) {
|
|
18767
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18768
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18769
|
+
map2.set(row.k, row.n);
|
|
18770
|
+
}
|
|
18771
|
+
return map2;
|
|
18772
|
+
}
|
|
18773
|
+
function mapRowsTolerant(rows, map2) {
|
|
18774
|
+
const out = [];
|
|
18775
|
+
for (const row of rows) {
|
|
18776
|
+
try {
|
|
18777
|
+
out.push(map2(row));
|
|
18778
|
+
} catch {
|
|
18779
|
+
}
|
|
18780
|
+
}
|
|
18781
|
+
return out;
|
|
18599
18782
|
}
|
|
18600
18783
|
|
|
18601
18784
|
// ../packages/persistence/src/repositories/activity.ts
|
|
@@ -18647,15 +18830,11 @@ function encodeCursor(payload) {
|
|
|
18647
18830
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
18648
18831
|
}
|
|
18649
18832
|
function decodeCursor(cursor) {
|
|
18650
|
-
|
|
18651
|
-
|
|
18652
|
-
|
|
18653
|
-
return parsed;
|
|
18654
|
-
}
|
|
18655
|
-
return null;
|
|
18656
|
-
} catch {
|
|
18657
|
-
return null;
|
|
18833
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18834
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18835
|
+
return parsed;
|
|
18658
18836
|
}
|
|
18837
|
+
return null;
|
|
18659
18838
|
}
|
|
18660
18839
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
18661
18840
|
session: "session",
|
|
@@ -18672,15 +18851,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
18672
18851
|
};
|
|
18673
18852
|
function safeParseStringArray(raw) {
|
|
18674
18853
|
if (!raw) return [];
|
|
18675
|
-
|
|
18676
|
-
|
|
18677
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
18678
|
-
} catch {
|
|
18679
|
-
return [];
|
|
18680
|
-
}
|
|
18681
|
-
}
|
|
18682
|
-
function toBool(raw) {
|
|
18683
|
-
return raw === 1 || raw === true;
|
|
18854
|
+
const parsed = safeJson(raw, null);
|
|
18855
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
18684
18856
|
}
|
|
18685
18857
|
function toHarness(raw) {
|
|
18686
18858
|
const parsed = Harness.safeParse(raw);
|
|
@@ -18733,8 +18905,8 @@ function buildAuditEvent(row) {
|
|
|
18733
18905
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
18734
18906
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
18735
18907
|
targetId: row.target_id,
|
|
18736
|
-
internal:
|
|
18737
|
-
flagged:
|
|
18908
|
+
internal: intToBool(row.internal),
|
|
18909
|
+
flagged: intToBool(row.flagged)
|
|
18738
18910
|
};
|
|
18739
18911
|
}
|
|
18740
18912
|
var TIMELINE_COLUMNS = `
|
|
@@ -18760,12 +18932,15 @@ var SqliteActivityRepository = class {
|
|
|
18760
18932
|
stats(tz) {
|
|
18761
18933
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
18762
18934
|
const { startMs, endMs } = window;
|
|
18763
|
-
const sessionsToday =
|
|
18935
|
+
const sessionsToday = countScalar(
|
|
18936
|
+
this.db,
|
|
18764
18937
|
`SELECT count(*) AS n FROM audit_events
|
|
18765
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
18766
|
-
|
|
18938
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
|
|
18939
|
+
[startMs, endMs]
|
|
18940
|
+
);
|
|
18767
18941
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
18768
|
-
const liveNow =
|
|
18942
|
+
const liveNow = countScalar(
|
|
18943
|
+
this.db,
|
|
18769
18944
|
`SELECT count(*) AS n FROM audit_events s
|
|
18770
18945
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
18771
18946
|
AND max(
|
|
@@ -18774,22 +18949,29 @@ var SqliteActivityRepository = class {
|
|
|
18774
18949
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
18775
18950
|
s.started_at
|
|
18776
18951
|
)
|
|
18777
|
-
) >=
|
|
18778
|
-
|
|
18779
|
-
|
|
18952
|
+
) >= ?`,
|
|
18953
|
+
[liveThreshold]
|
|
18954
|
+
);
|
|
18955
|
+
const toolCallsToday = countScalar(
|
|
18956
|
+
this.db,
|
|
18780
18957
|
`SELECT count(*) AS n FROM audit_events
|
|
18781
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
18782
|
-
|
|
18783
|
-
|
|
18958
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18959
|
+
[startMs, endMs]
|
|
18960
|
+
);
|
|
18961
|
+
const findingsToday = countScalar(
|
|
18962
|
+
this.db,
|
|
18784
18963
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
18785
18964
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18786
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
18787
|
-
|
|
18788
|
-
|
|
18965
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18966
|
+
[startMs, endMs]
|
|
18967
|
+
);
|
|
18968
|
+
const egressToday = countScalar(
|
|
18969
|
+
this.db,
|
|
18789
18970
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18790
18971
|
FROM audit_events
|
|
18791
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
18792
|
-
|
|
18972
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18973
|
+
[startMs, endMs]
|
|
18974
|
+
);
|
|
18793
18975
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
18794
18976
|
}
|
|
18795
18977
|
listSessions(query) {
|
|
@@ -18811,7 +18993,7 @@ var SqliteActivityRepository = class {
|
|
|
18811
18993
|
conditions.push("started_at <= ?");
|
|
18812
18994
|
params.push(toMs);
|
|
18813
18995
|
if (query.q) {
|
|
18814
|
-
const pattern =
|
|
18996
|
+
const pattern = containsPattern(query.q);
|
|
18815
18997
|
conditions.push(
|
|
18816
18998
|
`(content LIKE ? ESCAPE '\\'
|
|
18817
18999
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18830,8 +19012,9 @@ var SqliteActivityRepository = class {
|
|
|
18830
19012
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18831
19013
|
}
|
|
18832
19014
|
const limit = query.limit;
|
|
18833
|
-
const rows =
|
|
18834
|
-
|
|
19015
|
+
const rows = allRows(
|
|
19016
|
+
this.db.prepare(
|
|
19017
|
+
`SELECT id,
|
|
18835
19018
|
json_extract(attributes, '$.harness') AS harness,
|
|
18836
19019
|
content AS title,
|
|
18837
19020
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18844,7 +19027,9 @@ var SqliteActivityRepository = class {
|
|
|
18844
19027
|
WHERE ${conditions.join(" AND ")}
|
|
18845
19028
|
ORDER BY started_at DESC, id DESC
|
|
18846
19029
|
LIMIT ?`
|
|
18847
|
-
|
|
19030
|
+
),
|
|
19031
|
+
[...params, limit + 1]
|
|
19032
|
+
);
|
|
18848
19033
|
const hasMore = rows.length > limit;
|
|
18849
19034
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18850
19035
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18861,8 +19046,9 @@ var SqliteActivityRepository = class {
|
|
|
18861
19046
|
return Promise.resolve({ items, nextCursor });
|
|
18862
19047
|
}
|
|
18863
19048
|
getSession(sessionId) {
|
|
18864
|
-
const rootRow =
|
|
18865
|
-
|
|
19049
|
+
const rootRow = getRow(
|
|
19050
|
+
this.db.prepare(
|
|
19051
|
+
`SELECT id,
|
|
18866
19052
|
json_extract(attributes, '$.harness') AS harness,
|
|
18867
19053
|
content AS title,
|
|
18868
19054
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18879,47 +19065,66 @@ var SqliteActivityRepository = class {
|
|
|
18879
19065
|
FROM audit_events
|
|
18880
19066
|
WHERE id = ? AND event_type = 'session'
|
|
18881
19067
|
LIMIT 1`
|
|
18882
|
-
|
|
19068
|
+
),
|
|
19069
|
+
[sessionId]
|
|
19070
|
+
);
|
|
18883
19071
|
if (!rootRow) return Promise.resolve(null);
|
|
18884
|
-
const timelineRows =
|
|
18885
|
-
|
|
19072
|
+
const timelineRows = allRows(
|
|
19073
|
+
this.db.prepare(
|
|
19074
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18886
19075
|
FROM audit_events
|
|
18887
19076
|
WHERE id = ? OR root_session_id = ?
|
|
18888
19077
|
ORDER BY started_at ASC, id ASC`
|
|
18889
|
-
|
|
19078
|
+
),
|
|
19079
|
+
[sessionId, sessionId]
|
|
19080
|
+
);
|
|
18890
19081
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18891
|
-
const tokenRow =
|
|
18892
|
-
|
|
19082
|
+
const tokenRow = getRow(
|
|
19083
|
+
this.db.prepare(
|
|
19084
|
+
`SELECT
|
|
18893
19085
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18894
19086
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18895
19087
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18896
19088
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18897
19089
|
FROM audit_events
|
|
18898
19090
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
19091
|
+
),
|
|
19092
|
+
[sessionId]
|
|
19093
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
19094
|
+
const primaryModel = getRow(
|
|
19095
|
+
this.db.prepare(
|
|
19096
|
+
`SELECT model, provider FROM audit_events
|
|
18902
19097
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18903
19098
|
ORDER BY started_at ASC, id ASC
|
|
18904
19099
|
LIMIT 1`
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
19100
|
+
),
|
|
19101
|
+
[sessionId]
|
|
19102
|
+
);
|
|
19103
|
+
const toolRows = allRows(
|
|
19104
|
+
this.db.prepare(
|
|
19105
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18908
19106
|
count(*) AS n
|
|
18909
19107
|
FROM audit_events
|
|
18910
19108
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18911
19109
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
19110
|
+
),
|
|
19111
|
+
[sessionId]
|
|
19112
|
+
);
|
|
19113
|
+
const modelRows = allRows(
|
|
19114
|
+
this.db.prepare(
|
|
19115
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18915
19116
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18916
19117
|
ORDER BY model`
|
|
18917
|
-
|
|
19118
|
+
),
|
|
19119
|
+
[sessionId]
|
|
19120
|
+
);
|
|
18918
19121
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18919
|
-
const commits =
|
|
19122
|
+
const commits = countScalar(
|
|
19123
|
+
this.db,
|
|
18920
19124
|
`SELECT count(*) AS n FROM audit_events
|
|
18921
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18922
|
-
|
|
19125
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
19126
|
+
[sessionId]
|
|
19127
|
+
);
|
|
18923
19128
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18924
19129
|
turns: 0,
|
|
18925
19130
|
findings: 0,
|
|
@@ -18986,7 +19191,10 @@ var SqliteActivityRepository = class {
|
|
|
18986
19191
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18987
19192
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18988
19193
|
);
|
|
18989
|
-
const rows =
|
|
19194
|
+
const rows = allRows(
|
|
19195
|
+
stmt,
|
|
19196
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
19197
|
+
);
|
|
18990
19198
|
const seen = /* @__PURE__ */ new Set();
|
|
18991
19199
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18992
19200
|
return Promise.resolve([...seen]);
|
|
@@ -19009,23 +19217,23 @@ var SqliteActivityRepository = class {
|
|
|
19009
19217
|
conditions.push("started_at >= ?");
|
|
19010
19218
|
params.push(opts.fromMs);
|
|
19011
19219
|
}
|
|
19012
|
-
const rows =
|
|
19013
|
-
|
|
19220
|
+
const rows = allRows(
|
|
19221
|
+
this.db.prepare(
|
|
19222
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
19014
19223
|
FROM audit_events
|
|
19015
19224
|
WHERE ${conditions.join(" AND ")}`
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
}
|
|
19027
|
-
|
|
19028
|
-
return leaves;
|
|
19225
|
+
),
|
|
19226
|
+
params
|
|
19227
|
+
);
|
|
19228
|
+
return mapRowsTolerant(
|
|
19229
|
+
rows.filter(
|
|
19230
|
+
(row) => row.sessionId !== null
|
|
19231
|
+
),
|
|
19232
|
+
(row) => ({
|
|
19233
|
+
sessionId: row.sessionId,
|
|
19234
|
+
attributes: JSON.parse(row.attributes)
|
|
19235
|
+
})
|
|
19236
|
+
);
|
|
19029
19237
|
}
|
|
19030
19238
|
/**
|
|
19031
19239
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -19039,57 +19247,72 @@ var SqliteActivityRepository = class {
|
|
|
19039
19247
|
);
|
|
19040
19248
|
if (sessionIds.length === 0) return result;
|
|
19041
19249
|
const inClause = placeholders(sessionIds.length);
|
|
19042
|
-
const lastActivityRows =
|
|
19043
|
-
|
|
19250
|
+
const lastActivityRows = allRows(
|
|
19251
|
+
this.db.prepare(
|
|
19252
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
19044
19253
|
WHERE root_session_id IN (${inClause})
|
|
19045
19254
|
GROUP BY root_session_id`
|
|
19046
|
-
|
|
19255
|
+
),
|
|
19256
|
+
sessionIds
|
|
19257
|
+
);
|
|
19047
19258
|
for (const row of lastActivityRows) {
|
|
19048
19259
|
if (row.id === null) continue;
|
|
19049
19260
|
const entry = result.get(row.id);
|
|
19050
19261
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
19051
19262
|
}
|
|
19052
|
-
const turnsRows =
|
|
19053
|
-
|
|
19263
|
+
const turnsRows = allRows(
|
|
19264
|
+
this.db.prepare(
|
|
19265
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
19054
19266
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
19055
19267
|
GROUP BY root_session_id`
|
|
19056
|
-
|
|
19268
|
+
),
|
|
19269
|
+
sessionIds
|
|
19270
|
+
);
|
|
19057
19271
|
for (const row of turnsRows) {
|
|
19058
19272
|
if (row.id === null) continue;
|
|
19059
19273
|
const entry = result.get(row.id);
|
|
19060
19274
|
if (entry) entry.turns = row.n;
|
|
19061
19275
|
}
|
|
19062
|
-
const runKeyRows =
|
|
19063
|
-
|
|
19276
|
+
const runKeyRows = allRows(
|
|
19277
|
+
this.db.prepare(
|
|
19278
|
+
`SELECT root_session_id AS id,
|
|
19064
19279
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
19065
19280
|
FROM audit_events
|
|
19066
19281
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
19067
19282
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
19068
19283
|
GROUP BY root_session_id`
|
|
19069
|
-
|
|
19284
|
+
),
|
|
19285
|
+
sessionIds
|
|
19286
|
+
);
|
|
19070
19287
|
for (const row of runKeyRows) {
|
|
19071
19288
|
if (row.id === null) continue;
|
|
19072
19289
|
const entry = result.get(row.id);
|
|
19073
19290
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
19074
19291
|
}
|
|
19075
|
-
const findingsRows =
|
|
19076
|
-
|
|
19292
|
+
const findingsRows = allRows(
|
|
19293
|
+
this.db.prepare(
|
|
19294
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
19077
19295
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19078
19296
|
WHERE e.root_session_id IN (${inClause})
|
|
19079
19297
|
GROUP BY e.root_session_id`
|
|
19080
|
-
|
|
19298
|
+
),
|
|
19299
|
+
sessionIds
|
|
19300
|
+
);
|
|
19081
19301
|
for (const row of findingsRows) {
|
|
19082
19302
|
if (row.id === null) continue;
|
|
19083
19303
|
const entry = result.get(row.id);
|
|
19084
19304
|
if (entry) entry.findings = row.n;
|
|
19085
19305
|
}
|
|
19086
|
-
const sharesRows =
|
|
19087
|
-
|
|
19306
|
+
const sharesRows = allRows(
|
|
19307
|
+
this.db.prepare(
|
|
19308
|
+
`SELECT root_session_id AS id,
|
|
19088
19309
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
19089
19310
|
FROM audit_events
|
|
19090
19311
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
19091
19312
|
GROUP BY root_session_id`
|
|
19092
|
-
|
|
19313
|
+
),
|
|
19314
|
+
sessionIds
|
|
19315
|
+
);
|
|
19093
19316
|
for (const row of sharesRows) {
|
|
19094
19317
|
if (row.id === null) continue;
|
|
19095
19318
|
const entry = result.get(row.id);
|
|
@@ -19139,33 +19362,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
19139
19362
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
19140
19363
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
19141
19364
|
runInTransaction(fn) {
|
|
19142
|
-
this.db
|
|
19143
|
-
try {
|
|
19144
|
-
fn();
|
|
19145
|
-
this.db.exec("COMMIT");
|
|
19146
|
-
} catch (err) {
|
|
19147
|
-
this.db.exec("ROLLBACK");
|
|
19148
|
-
throw err;
|
|
19149
|
-
}
|
|
19365
|
+
withTransaction(this.db, fn);
|
|
19150
19366
|
}
|
|
19151
19367
|
insertAuditEvent(input) {
|
|
19152
19368
|
const row = toAuditEventRow(input);
|
|
19153
|
-
this.insertStmt.run(
|
|
19154
|
-
|
|
19155
|
-
|
|
19156
|
-
|
|
19157
|
-
|
|
19158
|
-
|
|
19159
|
-
|
|
19160
|
-
|
|
19161
|
-
|
|
19162
|
-
|
|
19163
|
-
|
|
19164
|
-
|
|
19165
|
-
|
|
19166
|
-
|
|
19167
|
-
|
|
19168
|
-
|
|
19369
|
+
this.insertStmt.run(
|
|
19370
|
+
bindParams({
|
|
19371
|
+
id: row.id,
|
|
19372
|
+
parentId: row.parentId,
|
|
19373
|
+
rootSessionId: row.rootSessionId,
|
|
19374
|
+
eventType: row.eventType,
|
|
19375
|
+
hostId: row.hostId,
|
|
19376
|
+
harnessId: row.harnessId,
|
|
19377
|
+
sourceProjectId: row.sourceProjectId,
|
|
19378
|
+
startedAt: row.startedAt,
|
|
19379
|
+
endedAt: row.endedAt,
|
|
19380
|
+
severity: row.severity,
|
|
19381
|
+
priority: row.priority,
|
|
19382
|
+
content: row.content,
|
|
19383
|
+
contentHash: row.contentHash,
|
|
19384
|
+
attributes: row.attributes
|
|
19385
|
+
})
|
|
19386
|
+
);
|
|
19169
19387
|
}
|
|
19170
19388
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
19171
19389
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -19179,22 +19397,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
19179
19397
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
19180
19398
|
if (!Number.isFinite(startedAt)) return;
|
|
19181
19399
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
19182
|
-
this.upsertLlmCallStmt.run(
|
|
19183
|
-
|
|
19184
|
-
|
|
19185
|
-
|
|
19186
|
-
|
|
19187
|
-
|
|
19188
|
-
|
|
19189
|
-
|
|
19190
|
-
|
|
19191
|
-
|
|
19192
|
-
|
|
19193
|
-
|
|
19194
|
-
|
|
19195
|
-
|
|
19196
|
-
|
|
19197
|
-
|
|
19400
|
+
this.upsertLlmCallStmt.run(
|
|
19401
|
+
bindParams({
|
|
19402
|
+
id,
|
|
19403
|
+
parentId: input.parentId,
|
|
19404
|
+
rootSessionId: input.rootSessionId,
|
|
19405
|
+
eventType: "llm_call",
|
|
19406
|
+
hostId: null,
|
|
19407
|
+
harnessId: null,
|
|
19408
|
+
sourceProjectId: null,
|
|
19409
|
+
startedAt,
|
|
19410
|
+
endedAt: null,
|
|
19411
|
+
severity: null,
|
|
19412
|
+
priority: null,
|
|
19413
|
+
content: null,
|
|
19414
|
+
contentHash: null,
|
|
19415
|
+
attributes: JSON.stringify(input.attributes)
|
|
19416
|
+
})
|
|
19417
|
+
);
|
|
19198
19418
|
}
|
|
19199
19419
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
19200
19420
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -19218,25 +19438,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
19218
19438
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
19219
19439
|
if (!Number.isFinite(startedAt)) return;
|
|
19220
19440
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
19221
|
-
this.insertStmt.run(
|
|
19222
|
-
|
|
19223
|
-
|
|
19224
|
-
|
|
19225
|
-
|
|
19226
|
-
|
|
19227
|
-
|
|
19228
|
-
|
|
19229
|
-
|
|
19230
|
-
|
|
19231
|
-
|
|
19232
|
-
|
|
19233
|
-
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
19441
|
+
this.insertStmt.run(
|
|
19442
|
+
bindParams({
|
|
19443
|
+
id,
|
|
19444
|
+
parentId: input.parentId,
|
|
19445
|
+
rootSessionId: input.rootSessionId,
|
|
19446
|
+
eventType: "tool_call",
|
|
19447
|
+
hostId: null,
|
|
19448
|
+
harnessId: null,
|
|
19449
|
+
sourceProjectId: null,
|
|
19450
|
+
startedAt,
|
|
19451
|
+
endedAt: null,
|
|
19452
|
+
severity: null,
|
|
19453
|
+
priority: null,
|
|
19454
|
+
content: null,
|
|
19455
|
+
contentHash: null,
|
|
19456
|
+
attributes: JSON.stringify(input.attributes)
|
|
19457
|
+
})
|
|
19458
|
+
);
|
|
19237
19459
|
}
|
|
19238
19460
|
findById(id) {
|
|
19239
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
19461
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
19462
|
+
id
|
|
19463
|
+
});
|
|
19240
19464
|
}
|
|
19241
19465
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
19242
19466
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -19246,14 +19470,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
19246
19470
|
sessionProvider(sessionId) {
|
|
19247
19471
|
const row = this.findById(sessionId);
|
|
19248
19472
|
if (!row?.attributes) return void 0;
|
|
19249
|
-
|
|
19250
|
-
|
|
19251
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
19252
|
-
const provider = parsed.provider;
|
|
19253
|
-
if (typeof provider === "string") return provider;
|
|
19254
|
-
}
|
|
19255
|
-
} catch {
|
|
19256
|
-
}
|
|
19473
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
19474
|
+
if (typeof provider === "string") return provider;
|
|
19257
19475
|
return void 0;
|
|
19258
19476
|
}
|
|
19259
19477
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -19262,11 +19480,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
19262
19480
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
19263
19481
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
19264
19482
|
llmCallLeaves() {
|
|
19265
|
-
return
|
|
19266
|
-
|
|
19483
|
+
return allRows(
|
|
19484
|
+
this.db.prepare(
|
|
19485
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
19267
19486
|
FROM audit_events
|
|
19268
19487
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
19269
|
-
|
|
19488
|
+
)
|
|
19489
|
+
);
|
|
19270
19490
|
}
|
|
19271
19491
|
};
|
|
19272
19492
|
|
|
@@ -19285,16 +19505,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
19285
19505
|
upsert(input) {
|
|
19286
19506
|
const id = classifiedDataId(input.class);
|
|
19287
19507
|
const row = toClassifiedDataRow(input, id);
|
|
19288
|
-
this.insertStmt.run(
|
|
19289
|
-
|
|
19290
|
-
|
|
19291
|
-
|
|
19292
|
-
|
|
19293
|
-
|
|
19508
|
+
this.insertStmt.run(
|
|
19509
|
+
bindParams({
|
|
19510
|
+
id: row.id,
|
|
19511
|
+
class: row.class,
|
|
19512
|
+
label: row.label,
|
|
19513
|
+
attributes: row.attributes
|
|
19514
|
+
})
|
|
19515
|
+
);
|
|
19294
19516
|
return id;
|
|
19295
19517
|
}
|
|
19296
19518
|
};
|
|
19297
19519
|
|
|
19520
|
+
// ../packages/persistence/src/repositories/config-scan.ts
|
|
19521
|
+
function latestConfigScan(db) {
|
|
19522
|
+
return getRow(
|
|
19523
|
+
db.prepare(
|
|
19524
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
19525
|
+
WHERE event_type = 'config_scan'
|
|
19526
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
19527
|
+
)
|
|
19528
|
+
);
|
|
19529
|
+
}
|
|
19530
|
+
|
|
19298
19531
|
// ../packages/persistence/src/repositories/config-inventory.ts
|
|
19299
19532
|
var SqliteConfigInventoryRepository = class {
|
|
19300
19533
|
constructor(db) {
|
|
@@ -19302,7 +19535,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
19302
19535
|
}
|
|
19303
19536
|
db;
|
|
19304
19537
|
report() {
|
|
19305
|
-
const scan2 = this.
|
|
19538
|
+
const scan2 = latestConfigScan(this.db);
|
|
19306
19539
|
if (!scan2) {
|
|
19307
19540
|
return {
|
|
19308
19541
|
scannedAt: null,
|
|
@@ -19313,17 +19546,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
19313
19546
|
topics: []
|
|
19314
19547
|
};
|
|
19315
19548
|
}
|
|
19316
|
-
const rows =
|
|
19317
|
-
|
|
19549
|
+
const rows = allRows(
|
|
19550
|
+
this.db.prepare(
|
|
19551
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
19318
19552
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
19319
19553
|
ORDER BY object_type, title`
|
|
19320
|
-
|
|
19321
|
-
|
|
19322
|
-
|
|
19554
|
+
),
|
|
19555
|
+
{ startedAt: scan2.started_at }
|
|
19556
|
+
);
|
|
19557
|
+
const findings = allRows(
|
|
19558
|
+
this.db.prepare(
|
|
19559
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
19323
19560
|
FROM inspection_findings f
|
|
19324
19561
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19325
19562
|
WHERE f.audit_event_id = :scanId`
|
|
19326
|
-
|
|
19563
|
+
),
|
|
19564
|
+
{ scanId: scan2.id }
|
|
19565
|
+
);
|
|
19327
19566
|
const skills = [];
|
|
19328
19567
|
const hooks = [];
|
|
19329
19568
|
const mcpServers = [];
|
|
@@ -19352,7 +19591,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
19352
19591
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
19353
19592
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
19354
19593
|
trustOverrides() {
|
|
19355
|
-
const rows =
|
|
19594
|
+
const rows = allRows(
|
|
19595
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
19596
|
+
);
|
|
19356
19597
|
const map2 = /* @__PURE__ */ new Map();
|
|
19357
19598
|
for (const row of rows) {
|
|
19358
19599
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -19361,13 +19602,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
19361
19602
|
}
|
|
19362
19603
|
return map2;
|
|
19363
19604
|
}
|
|
19364
|
-
latestScan() {
|
|
19365
|
-
return this.db.prepare(
|
|
19366
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
19367
|
-
WHERE event_type = 'config_scan'
|
|
19368
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
19369
|
-
).get();
|
|
19370
|
-
}
|
|
19371
19605
|
};
|
|
19372
19606
|
function toSkillItem(row, bag) {
|
|
19373
19607
|
const item = {
|
|
@@ -19478,22 +19712,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
19478
19712
|
return topics;
|
|
19479
19713
|
}
|
|
19480
19714
|
function countScanErrors(attributes) {
|
|
19481
|
-
|
|
19482
|
-
|
|
19483
|
-
const parsed = JSON.parse(attributes);
|
|
19484
|
-
const errors = parsed?.errors;
|
|
19485
|
-
return typeof errors === "number" ? errors : 0;
|
|
19486
|
-
} catch {
|
|
19487
|
-
return 0;
|
|
19488
|
-
}
|
|
19715
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
19716
|
+
return typeof errors === "number" ? errors : 0;
|
|
19489
19717
|
}
|
|
19490
19718
|
function parseBag(raw) {
|
|
19491
|
-
|
|
19492
|
-
const parsed = JSON.parse(raw);
|
|
19493
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
19494
|
-
} catch {
|
|
19495
|
-
}
|
|
19496
|
-
return void 0;
|
|
19719
|
+
return parseJsonObject(raw);
|
|
19497
19720
|
}
|
|
19498
19721
|
function str(value) {
|
|
19499
19722
|
return typeof value === "string" ? value : void 0;
|
|
@@ -19502,12 +19725,7 @@ function str(value) {
|
|
|
19502
19725
|
// ../packages/persistence/src/repositories/detections.ts
|
|
19503
19726
|
var DAY_MS2 = 864e5;
|
|
19504
19727
|
function parseRules(rulesJson) {
|
|
19505
|
-
|
|
19506
|
-
try {
|
|
19507
|
-
raw = JSON.parse(rulesJson);
|
|
19508
|
-
} catch {
|
|
19509
|
-
return [];
|
|
19510
|
-
}
|
|
19728
|
+
const raw = safeJson(rulesJson, []);
|
|
19511
19729
|
if (!Array.isArray(raw)) return [];
|
|
19512
19730
|
const rules = [];
|
|
19513
19731
|
for (const entry of raw) {
|
|
@@ -19526,11 +19744,13 @@ var SqliteDetectionsRepository = class {
|
|
|
19526
19744
|
db;
|
|
19527
19745
|
now;
|
|
19528
19746
|
listDetections(query) {
|
|
19529
|
-
const rows =
|
|
19530
|
-
|
|
19747
|
+
const rows = allRows(
|
|
19748
|
+
this.db.prepare(
|
|
19749
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
19531
19750
|
rules_json AS rulesJson
|
|
19532
19751
|
FROM installed_packs`
|
|
19533
|
-
|
|
19752
|
+
)
|
|
19753
|
+
);
|
|
19534
19754
|
const available = this.availableByPack();
|
|
19535
19755
|
const summaries = rows.map((r) => {
|
|
19536
19756
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -19539,7 +19759,7 @@ var SqliteDetectionsRepository = class {
|
|
|
19539
19759
|
packId: r.packId,
|
|
19540
19760
|
version: r.version,
|
|
19541
19761
|
name: r.name,
|
|
19542
|
-
enabled: r.enabled
|
|
19762
|
+
enabled: intToBool(r.enabled),
|
|
19543
19763
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
19544
19764
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
19545
19765
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -19556,19 +19776,23 @@ var SqliteDetectionsRepository = class {
|
|
|
19556
19776
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
19557
19777
|
// detail call; the table is a handful of rows).
|
|
19558
19778
|
availableByPack() {
|
|
19559
|
-
const rows =
|
|
19560
|
-
|
|
19779
|
+
const rows = allRows(
|
|
19780
|
+
this.db.prepare(
|
|
19781
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
19561
19782
|
FROM available_packs`
|
|
19562
|
-
|
|
19783
|
+
)
|
|
19784
|
+
);
|
|
19563
19785
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
19564
19786
|
}
|
|
19565
19787
|
getDetectionStats() {
|
|
19566
|
-
const rows =
|
|
19788
|
+
const rows = allRows(
|
|
19789
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
19790
|
+
);
|
|
19567
19791
|
let rules = 0;
|
|
19568
19792
|
let active = 0;
|
|
19569
19793
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
19570
19794
|
for (const r of rows) {
|
|
19571
|
-
if (r.enabled
|
|
19795
|
+
if (intToBool(r.enabled)) active += 1;
|
|
19572
19796
|
const parsed = parseRules(r.rulesJson);
|
|
19573
19797
|
rules += parsed.length;
|
|
19574
19798
|
for (const rule of parsed) {
|
|
@@ -19586,12 +19810,15 @@ var SqliteDetectionsRepository = class {
|
|
|
19586
19810
|
const parts = splitDetectionId(id);
|
|
19587
19811
|
if (!parts) return Promise.resolve(null);
|
|
19588
19812
|
const { namespace, packId } = parts;
|
|
19589
|
-
const row =
|
|
19590
|
-
|
|
19813
|
+
const row = getRow(
|
|
19814
|
+
this.db.prepare(
|
|
19815
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
19591
19816
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
19592
19817
|
FROM installed_packs
|
|
19593
19818
|
WHERE namespace = ? AND pack_id = ?`
|
|
19594
|
-
|
|
19819
|
+
),
|
|
19820
|
+
[namespace, packId]
|
|
19821
|
+
);
|
|
19595
19822
|
if (!row) return Promise.resolve(null);
|
|
19596
19823
|
const rules = parseRules(row.rulesJson);
|
|
19597
19824
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -19609,7 +19836,7 @@ var SqliteDetectionsRepository = class {
|
|
|
19609
19836
|
packId: row.packId,
|
|
19610
19837
|
version: row.version,
|
|
19611
19838
|
name: row.name,
|
|
19612
|
-
enabled: row.enabled
|
|
19839
|
+
enabled: intToBool(row.enabled),
|
|
19613
19840
|
rules,
|
|
19614
19841
|
updatedAt: new Date(row.updatedAt),
|
|
19615
19842
|
policyId: row.policyId
|
|
@@ -19624,13 +19851,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19624
19851
|
countFindingsLast30d(ruleIds) {
|
|
19625
19852
|
if (ruleIds.length === 0) return 0;
|
|
19626
19853
|
const since = this.now() - 30 * DAY_MS2;
|
|
19627
|
-
const
|
|
19628
|
-
|
|
19629
|
-
|
|
19854
|
+
const inClause = placeholders(ruleIds.length);
|
|
19855
|
+
return countScalar(
|
|
19856
|
+
this.db,
|
|
19857
|
+
`SELECT count(*) AS n
|
|
19630
19858
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19631
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
19632
|
-
|
|
19633
|
-
|
|
19859
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19860
|
+
[since, ...ruleIds]
|
|
19861
|
+
);
|
|
19634
19862
|
}
|
|
19635
19863
|
};
|
|
19636
19864
|
|
|
@@ -19647,16 +19875,17 @@ var SqliteEventsRepository = class {
|
|
|
19647
19875
|
insertStmt;
|
|
19648
19876
|
insertEvent(event) {
|
|
19649
19877
|
const row = toEventRow(event);
|
|
19650
|
-
this.insertStmt.run(
|
|
19651
|
-
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19656
|
-
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
19878
|
+
this.insertStmt.run(
|
|
19879
|
+
bindParams({
|
|
19880
|
+
id: row.id,
|
|
19881
|
+
sourceTool: row.sourceTool,
|
|
19882
|
+
kind: row.kind,
|
|
19883
|
+
occurredAt: row.occurredAt,
|
|
19884
|
+
contentHash: row.contentHash,
|
|
19885
|
+
content: row.content,
|
|
19886
|
+
metadata: row.metadata
|
|
19887
|
+
})
|
|
19888
|
+
);
|
|
19660
19889
|
}
|
|
19661
19890
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
19662
19891
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -19664,13 +19893,23 @@ var SqliteEventsRepository = class {
|
|
|
19664
19893
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19665
19894
|
// async EventsReadPort contract.
|
|
19666
19895
|
contentHashes() {
|
|
19667
|
-
const rows =
|
|
19896
|
+
const rows = allRows(
|
|
19897
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19898
|
+
);
|
|
19668
19899
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19669
19900
|
}
|
|
19670
19901
|
};
|
|
19671
19902
|
|
|
19672
19903
|
// ../packages/persistence/src/repositories/exceptions.ts
|
|
19673
19904
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
19905
|
+
|
|
19906
|
+
// ../packages/persistence/src/internal/sqlite-errors.ts
|
|
19907
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19908
|
+
function isUniqueConstraintError(err) {
|
|
19909
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19910
|
+
}
|
|
19911
|
+
|
|
19912
|
+
// ../packages/persistence/src/repositories/exceptions.ts
|
|
19674
19913
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
19675
19914
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
19676
19915
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -19691,10 +19930,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
19691
19930
|
this.name = "AmbiguousExceptionIdError";
|
|
19692
19931
|
}
|
|
19693
19932
|
};
|
|
19694
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19695
|
-
function isUniqueConstraintError(err) {
|
|
19696
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19697
|
-
}
|
|
19698
19933
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
19699
19934
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
19700
19935
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -19750,10 +19985,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19750
19985
|
this.insertExceptionRow(id, input, now);
|
|
19751
19986
|
} catch (err) {
|
|
19752
19987
|
if (!isUniqueConstraintError(err)) throw err;
|
|
19753
|
-
|
|
19754
|
-
|
|
19755
|
-
|
|
19756
|
-
|
|
19988
|
+
withTransaction(
|
|
19989
|
+
this.db,
|
|
19990
|
+
() => {
|
|
19991
|
+
const superseded = this.db.prepare(
|
|
19992
|
+
`UPDATE exceptions
|
|
19757
19993
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
19758
19994
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
19759
19995
|
updated_at = :now
|
|
@@ -19761,24 +19997,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19761
19997
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
19762
19998
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
19763
19999
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
19764
|
-
|
|
19765
|
-
|
|
19766
|
-
|
|
19767
|
-
|
|
19768
|
-
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
|
|
19772
|
-
|
|
19773
|
-
|
|
19774
|
-
|
|
19775
|
-
|
|
19776
|
-
|
|
19777
|
-
|
|
19778
|
-
|
|
19779
|
-
|
|
20000
|
+
).run({
|
|
20001
|
+
now,
|
|
20002
|
+
revokedBy: input.createdBy,
|
|
20003
|
+
ruleId: input.ruleId,
|
|
20004
|
+
valueFingerprint: input.valueFingerprint,
|
|
20005
|
+
keyVersion: input.keyVersion
|
|
20006
|
+
});
|
|
20007
|
+
if (Number(superseded.changes) !== 1) {
|
|
20008
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
20009
|
+
}
|
|
20010
|
+
this.insertExceptionRow(id, input, now);
|
|
20011
|
+
},
|
|
20012
|
+
"IMMEDIATE"
|
|
20013
|
+
);
|
|
20014
|
+
}
|
|
20015
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
20016
|
+
id
|
|
20017
|
+
});
|
|
20018
|
+
if (row === void 0) {
|
|
20019
|
+
throw new Error("exception row not found immediately after insert");
|
|
19780
20020
|
}
|
|
19781
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
19782
20021
|
return parseExceptionRow(row);
|
|
19783
20022
|
}
|
|
19784
20023
|
insertExceptionRow(id, input, now) {
|
|
@@ -19816,14 +20055,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19816
20055
|
*/
|
|
19817
20056
|
list(opts) {
|
|
19818
20057
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19819
|
-
const rows =
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
} catch {
|
|
19825
|
-
}
|
|
19826
|
-
}
|
|
20058
|
+
const rows = allRows(
|
|
20059
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
20060
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
20061
|
+
);
|
|
20062
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19827
20063
|
return Promise.resolve(exceptions);
|
|
19828
20064
|
}
|
|
19829
20065
|
/**
|
|
@@ -19833,7 +20069,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19833
20069
|
*/
|
|
19834
20070
|
getByIdPrefix(prefix) {
|
|
19835
20071
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19836
|
-
const rows =
|
|
20072
|
+
const rows = allRows(
|
|
20073
|
+
this.db.prepare(
|
|
20074
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
20075
|
+
),
|
|
20076
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
20077
|
+
);
|
|
19837
20078
|
if (rows.length > 1) {
|
|
19838
20079
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19839
20080
|
}
|
|
@@ -19875,30 +20116,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19875
20116
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19876
20117
|
*/
|
|
19877
20118
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19878
|
-
const rows =
|
|
19879
|
-
|
|
20119
|
+
const rows = allRows(
|
|
20120
|
+
this.db.prepare(
|
|
20121
|
+
`SELECT * FROM exceptions
|
|
19880
20122
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19881
20123
|
ORDER BY created_at DESC, rowid DESC`
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
19889
|
-
|
|
19890
|
-
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
|
|
19894
|
-
|
|
19895
|
-
|
|
19896
|
-
|
|
19897
|
-
|
|
19898
|
-
);
|
|
19899
|
-
} catch {
|
|
19900
|
-
}
|
|
19901
|
-
}
|
|
20124
|
+
),
|
|
20125
|
+
{ keyVersion, now }
|
|
20126
|
+
);
|
|
20127
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
20128
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
20129
|
+
return ExceptionBundleEntry.parse({
|
|
20130
|
+
id: row.id,
|
|
20131
|
+
ruleId: row.rule_id,
|
|
20132
|
+
valueFingerprint: row.value_fingerprint,
|
|
20133
|
+
keyVersion: row.key_version,
|
|
20134
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
20135
|
+
maxUses: row.max_uses,
|
|
20136
|
+
useCount: row.use_count,
|
|
20137
|
+
conditions
|
|
20138
|
+
});
|
|
20139
|
+
});
|
|
19902
20140
|
return Promise.resolve(entries);
|
|
19903
20141
|
}
|
|
19904
20142
|
/**
|
|
@@ -19925,11 +20163,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19925
20163
|
}
|
|
19926
20164
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19927
20165
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19928
|
-
const rows =
|
|
19929
|
-
|
|
20166
|
+
const rows = allRows(
|
|
20167
|
+
this.db.prepare(
|
|
20168
|
+
`SELECT * FROM blocked_detections
|
|
19930
20169
|
WHERE blocked_at > :cutoff
|
|
19931
20170
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19932
|
-
|
|
20171
|
+
),
|
|
20172
|
+
{ cutoff: Date.now() - windowMs }
|
|
20173
|
+
);
|
|
19933
20174
|
return Promise.resolve(
|
|
19934
20175
|
rows.map((row) => ({
|
|
19935
20176
|
reference: row.reference,
|
|
@@ -20009,7 +20250,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
20009
20250
|
)`;
|
|
20010
20251
|
|
|
20011
20252
|
// ../packages/persistence/src/repositories/findings.ts
|
|
20012
|
-
var
|
|
20253
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
20254
|
+
var CONCAT_SEP = ",";
|
|
20255
|
+
var TUPLE_SEP = "|";
|
|
20256
|
+
function splitConcat(value) {
|
|
20257
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
20258
|
+
}
|
|
20013
20259
|
function deriveInstanceStatus(row) {
|
|
20014
20260
|
return deriveFindingStatus({
|
|
20015
20261
|
kind: row.kind,
|
|
@@ -20077,13 +20323,16 @@ var SqliteFindingsRepository = class {
|
|
|
20077
20323
|
}
|
|
20078
20324
|
recentFindings(opts) {
|
|
20079
20325
|
const limit = opts?.limit ?? 50;
|
|
20080
|
-
const rows =
|
|
20081
|
-
|
|
20326
|
+
const rows = allRows(
|
|
20327
|
+
this.db.prepare(
|
|
20328
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
20082
20329
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
20083
20330
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
20084
20331
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
20085
20332
|
LIMIT :limit`
|
|
20086
|
-
|
|
20333
|
+
),
|
|
20334
|
+
{ limit }
|
|
20335
|
+
);
|
|
20087
20336
|
return Promise.resolve(
|
|
20088
20337
|
rows.map((r) => ({
|
|
20089
20338
|
id: r.id,
|
|
@@ -20106,20 +20355,47 @@ var SqliteFindingsRepository = class {
|
|
|
20106
20355
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
20107
20356
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
20108
20357
|
* reflect the full filtered set; `items` is the requested
|
|
20109
|
-
* page (default
|
|
20358
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
20359
|
+
*
|
|
20360
|
+
* Two reads, neither of which materializes a row per finding:
|
|
20361
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
20362
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
20363
|
+
* latest, search text);
|
|
20364
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
20365
|
+
* populate `instances` for the table's expanded rows.
|
|
20366
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
20367
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
20368
|
+
* rule is ever restated in SQL.
|
|
20110
20369
|
*/
|
|
20111
20370
|
listGroupedFindings(query) {
|
|
20112
|
-
const
|
|
20113
|
-
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
|
|
20117
|
-
|
|
20118
|
-
|
|
20119
|
-
|
|
20120
|
-
|
|
20121
|
-
|
|
20122
|
-
|
|
20371
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
|
|
20372
|
+
const rows = allRows(
|
|
20373
|
+
this.db.prepare(
|
|
20374
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
20375
|
+
occurred_at, source_tool, repo, file, kind, finding_key, latest_status
|
|
20376
|
+
FROM (
|
|
20377
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
20378
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
20379
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
20380
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
20381
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
20382
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
20383
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
20384
|
+
latest.status AS latest_status,
|
|
20385
|
+
ROW_NUMBER() OVER (
|
|
20386
|
+
PARTITION BY f.rule_id
|
|
20387
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
20388
|
+
) AS rn
|
|
20389
|
+
FROM findings f
|
|
20390
|
+
JOIN events e ON e.id = f.event_id
|
|
20391
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20392
|
+
ON latest.finding_key = f.finding_key
|
|
20393
|
+
)
|
|
20394
|
+
WHERE rn <= :cap
|
|
20395
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
20396
|
+
),
|
|
20397
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP }
|
|
20398
|
+
);
|
|
20123
20399
|
const groupable = rows.map((r) => ({
|
|
20124
20400
|
id: r.id,
|
|
20125
20401
|
ruleId: r.rule_id,
|
|
@@ -20134,7 +20410,7 @@ var SqliteFindingsRepository = class {
|
|
|
20134
20410
|
file: r.file ?? "",
|
|
20135
20411
|
status: deriveInstanceStatus(r)
|
|
20136
20412
|
}));
|
|
20137
|
-
const allGroups = buildFindingGroups(groupable);
|
|
20413
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
20138
20414
|
const filterOpts = {
|
|
20139
20415
|
severity: query.severity,
|
|
20140
20416
|
providers: query.provider,
|
|
@@ -20152,42 +20428,122 @@ var SqliteFindingsRepository = class {
|
|
|
20152
20428
|
const items = sorted.slice(0, limit);
|
|
20153
20429
|
return Promise.resolve({ totals, facets, items, nextCursor: null });
|
|
20154
20430
|
}
|
|
20431
|
+
/**
|
|
20432
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
20433
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
20434
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
20435
|
+
*
|
|
20436
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
20437
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
20438
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
20439
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
20440
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
20441
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
20442
|
+
* a group's row stays small however many findings it holds.
|
|
20443
|
+
*
|
|
20444
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
20445
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
20446
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
20447
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
20448
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
20449
|
+
* match an instance outside the preview, which searching the preview alone
|
|
20450
|
+
* would silently lose, so it is fetched only when the request actually
|
|
20451
|
+
* carries a `q`.
|
|
20452
|
+
*/
|
|
20453
|
+
groupAggregates(withSearchText) {
|
|
20454
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
20455
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
|
|
20456
|
+
const rows = this.db.prepare(
|
|
20457
|
+
`SELECT f.rule_id AS rule_id,
|
|
20458
|
+
count(*) AS instance_count,
|
|
20459
|
+
max(e.occurred_at) AS latest_at,
|
|
20460
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
20461
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
20462
|
+
group_concat(DISTINCT (
|
|
20463
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
20464
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20465
|
+
coalesce(latest.status, '')
|
|
20466
|
+
)) AS status_inputs
|
|
20467
|
+
${searchTextColumns}
|
|
20468
|
+
FROM findings f
|
|
20469
|
+
JOIN events e ON e.id = f.event_id
|
|
20470
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20471
|
+
ON latest.finding_key = f.finding_key
|
|
20472
|
+
GROUP BY f.rule_id`
|
|
20473
|
+
).all();
|
|
20474
|
+
return new Map(
|
|
20475
|
+
rows.map((r) => [
|
|
20476
|
+
r.rule_id,
|
|
20477
|
+
{
|
|
20478
|
+
instanceCount: r.instance_count,
|
|
20479
|
+
sourceTools: splitConcat(r.source_tools),
|
|
20480
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
20481
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20482
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20483
|
+
return {
|
|
20484
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20485
|
+
// so the marker stands in for the key itself (never rendered).
|
|
20486
|
+
kind,
|
|
20487
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20488
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20489
|
+
};
|
|
20490
|
+
}),
|
|
20491
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
20492
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
20493
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
20494
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
20495
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
20496
|
+
// and skip priming a haystack nothing will read.
|
|
20497
|
+
...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
|
|
20498
|
+
}
|
|
20499
|
+
])
|
|
20500
|
+
);
|
|
20501
|
+
}
|
|
20155
20502
|
healthSummary() {
|
|
20156
|
-
const total = this.db
|
|
20503
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
20157
20504
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20158
|
-
const grouped =
|
|
20505
|
+
const grouped = allRows(
|
|
20506
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
20507
|
+
);
|
|
20159
20508
|
for (const row of grouped) {
|
|
20160
20509
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
20161
20510
|
}
|
|
20162
20511
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20163
|
-
const sevRows =
|
|
20164
|
-
|
|
20512
|
+
const sevRows = allRows(
|
|
20513
|
+
this.db.prepare(
|
|
20514
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
20165
20515
|
FROM findings f
|
|
20166
20516
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20167
20517
|
ON latest.finding_key = f.finding_key
|
|
20168
20518
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
20169
20519
|
GROUP BY f.severity`
|
|
20170
|
-
|
|
20520
|
+
)
|
|
20521
|
+
);
|
|
20171
20522
|
for (const row of sevRows) {
|
|
20172
20523
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
20173
20524
|
}
|
|
20174
20525
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
20175
|
-
const enabledRows =
|
|
20176
|
-
|
|
20526
|
+
const enabledRows = allRows(
|
|
20527
|
+
this.db.prepare(
|
|
20528
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
20177
20529
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
20178
|
-
|
|
20530
|
+
)
|
|
20531
|
+
);
|
|
20179
20532
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
20180
20533
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
20181
20534
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
20182
20535
|
}
|
|
20183
20536
|
activityByDay(days = 7) {
|
|
20184
20537
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20185
|
-
const rows =
|
|
20186
|
-
|
|
20538
|
+
const rows = allRows(
|
|
20539
|
+
this.db.prepare(
|
|
20540
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20187
20541
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
20188
20542
|
WHERE e.occurred_at >= :since
|
|
20189
20543
|
GROUP BY day, f.action_taken`
|
|
20190
|
-
|
|
20544
|
+
),
|
|
20545
|
+
{ since }
|
|
20546
|
+
);
|
|
20191
20547
|
const buckets = /* @__PURE__ */ new Map();
|
|
20192
20548
|
for (let i = 0; i < days; i++) {
|
|
20193
20549
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -20259,17 +20615,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20259
20615
|
insertStmt;
|
|
20260
20616
|
insertFinding(input) {
|
|
20261
20617
|
const row = toInspectionFindingRow(input);
|
|
20262
|
-
this.insertStmt.run(
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20267
|
-
|
|
20268
|
-
|
|
20269
|
-
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20618
|
+
this.insertStmt.run(
|
|
20619
|
+
bindParams({
|
|
20620
|
+
id: row.id,
|
|
20621
|
+
auditEventId: row.auditEventId,
|
|
20622
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
20623
|
+
classifiedDataId: row.classifiedDataId,
|
|
20624
|
+
spanStart: row.spanStart,
|
|
20625
|
+
spanEnd: row.spanEnd,
|
|
20626
|
+
maskedMatch: row.maskedMatch,
|
|
20627
|
+
actionTaken: row.actionTaken,
|
|
20628
|
+
confidence: row.confidence
|
|
20629
|
+
})
|
|
20630
|
+
);
|
|
20273
20631
|
}
|
|
20274
20632
|
};
|
|
20275
20633
|
|
|
@@ -20345,12 +20703,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
20345
20703
|
}
|
|
20346
20704
|
function ruleIdsOf(rulesJson) {
|
|
20347
20705
|
const ids = /* @__PURE__ */ new Set();
|
|
20348
|
-
|
|
20349
|
-
try {
|
|
20350
|
-
raw = JSON.parse(rulesJson);
|
|
20351
|
-
} catch {
|
|
20352
|
-
return ids;
|
|
20353
|
-
}
|
|
20706
|
+
const raw = safeJson(rulesJson, []);
|
|
20354
20707
|
if (!Array.isArray(raw)) return ids;
|
|
20355
20708
|
for (const entry of raw) {
|
|
20356
20709
|
if (entry && typeof entry === "object") {
|
|
@@ -20423,47 +20776,48 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20423
20776
|
}));
|
|
20424
20777
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
20425
20778
|
const now = Date.now();
|
|
20426
|
-
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
const
|
|
20432
|
-
|
|
20433
|
-
namespace: row.namespace,
|
|
20434
|
-
packId: row.packId,
|
|
20435
|
-
version: row.version,
|
|
20436
|
-
name: row.name,
|
|
20437
|
-
rulesJson: row.rulesJson,
|
|
20438
|
-
now
|
|
20439
|
-
};
|
|
20440
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
20441
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20442
|
-
this.upsertAvailableStmt.run({
|
|
20443
|
-
...params,
|
|
20779
|
+
withTransaction(
|
|
20780
|
+
this.db,
|
|
20781
|
+
() => {
|
|
20782
|
+
const mirror = this.mirrorState();
|
|
20783
|
+
let behind = false;
|
|
20784
|
+
for (const row of rows) {
|
|
20785
|
+
const params = {
|
|
20444
20786
|
id: randomUUID3(),
|
|
20445
|
-
|
|
20446
|
-
|
|
20447
|
-
|
|
20448
|
-
|
|
20787
|
+
namespace: row.namespace,
|
|
20788
|
+
packId: row.packId,
|
|
20789
|
+
version: row.version,
|
|
20790
|
+
name: row.name,
|
|
20791
|
+
rulesJson: row.rulesJson,
|
|
20792
|
+
now
|
|
20793
|
+
};
|
|
20794
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
20795
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20796
|
+
this.upsertAvailableStmt.run({
|
|
20797
|
+
...params,
|
|
20798
|
+
id: randomUUID3(),
|
|
20799
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
20800
|
+
});
|
|
20801
|
+
} else {
|
|
20802
|
+
behind = true;
|
|
20803
|
+
}
|
|
20804
|
+
this.insertMissingStmt.run(params);
|
|
20449
20805
|
}
|
|
20450
|
-
this.
|
|
20451
|
-
}
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
} catch (err) {
|
|
20455
|
-
this.db.exec("ROLLBACK");
|
|
20456
|
-
throw err;
|
|
20457
|
-
}
|
|
20806
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20807
|
+
},
|
|
20808
|
+
"IMMEDIATE"
|
|
20809
|
+
);
|
|
20458
20810
|
} catch {
|
|
20459
20811
|
}
|
|
20460
20812
|
}
|
|
20461
20813
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
20462
20814
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
20463
20815
|
mirrorState() {
|
|
20464
|
-
const rows =
|
|
20465
|
-
|
|
20466
|
-
|
|
20816
|
+
const rows = allRows(
|
|
20817
|
+
this.db.prepare(
|
|
20818
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20819
|
+
)
|
|
20820
|
+
);
|
|
20467
20821
|
return new Map(
|
|
20468
20822
|
rows.map((r) => [
|
|
20469
20823
|
`${r.namespace}/${r.packId}`,
|
|
@@ -20475,7 +20829,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20475
20829
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
20476
20830
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
20477
20831
|
pruneAvailable(keep) {
|
|
20478
|
-
const rows =
|
|
20832
|
+
const rows = allRows(
|
|
20833
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20834
|
+
);
|
|
20479
20835
|
const keepSet = new Set(keep);
|
|
20480
20836
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
20481
20837
|
for (const r of rows) {
|
|
@@ -20502,11 +20858,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20502
20858
|
if (this.db.isTransaction) {
|
|
20503
20859
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
20504
20860
|
}
|
|
20505
|
-
|
|
20506
|
-
|
|
20507
|
-
this.db
|
|
20508
|
-
|
|
20509
|
-
|
|
20861
|
+
let changed = false;
|
|
20862
|
+
withTransaction(
|
|
20863
|
+
this.db,
|
|
20864
|
+
() => {
|
|
20865
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20866
|
+
const res = this.db.prepare(
|
|
20867
|
+
`UPDATE installed_packs SET
|
|
20510
20868
|
version = (SELECT a.version FROM available_packs a
|
|
20511
20869
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
20512
20870
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -20517,17 +20875,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20517
20875
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
20518
20876
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
20519
20877
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
20520
|
-
|
|
20521
|
-
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
|
|
20526
|
-
|
|
20527
|
-
} catch {
|
|
20528
|
-
}
|
|
20529
|
-
throw err;
|
|
20530
|
-
}
|
|
20878
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20879
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20880
|
+
changed = Number(res.changes) > 0;
|
|
20881
|
+
},
|
|
20882
|
+
"IMMEDIATE"
|
|
20883
|
+
);
|
|
20884
|
+
return changed;
|
|
20531
20885
|
}
|
|
20532
20886
|
/**
|
|
20533
20887
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -20541,9 +20895,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20541
20895
|
* JSON-level failure therefore counts as invalid.
|
|
20542
20896
|
*/
|
|
20543
20897
|
installedRuleset() {
|
|
20544
|
-
const rows =
|
|
20545
|
-
|
|
20546
|
-
|
|
20898
|
+
const rows = allRows(
|
|
20899
|
+
this.db.prepare(
|
|
20900
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20901
|
+
)
|
|
20902
|
+
);
|
|
20547
20903
|
const out = {
|
|
20548
20904
|
installedPacks: rows.length,
|
|
20549
20905
|
enabledPacks: 0,
|
|
@@ -20552,7 +20908,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20552
20908
|
ruleActions: /* @__PURE__ */ new Map()
|
|
20553
20909
|
};
|
|
20554
20910
|
for (const row of rows) {
|
|
20555
|
-
if (row.enabled
|
|
20911
|
+
if (!intToBool(row.enabled)) continue;
|
|
20556
20912
|
out.enabledPacks += 1;
|
|
20557
20913
|
const action = policyIdToAction(row.policyId);
|
|
20558
20914
|
let raw;
|
|
@@ -20587,9 +20943,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20587
20943
|
* running max would mask a genuinely-newer parseable stamp.
|
|
20588
20944
|
*/
|
|
20589
20945
|
newestRecordedBinary() {
|
|
20590
|
-
const rows =
|
|
20591
|
-
|
|
20592
|
-
|
|
20946
|
+
const rows = allRows(
|
|
20947
|
+
this.db.prepare(
|
|
20948
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20949
|
+
)
|
|
20950
|
+
);
|
|
20593
20951
|
let newest = null;
|
|
20594
20952
|
for (const row of rows) {
|
|
20595
20953
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -20604,13 +20962,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20604
20962
|
return newest;
|
|
20605
20963
|
}
|
|
20606
20964
|
counts() {
|
|
20607
|
-
const row =
|
|
20608
|
-
|
|
20965
|
+
const row = getRow(
|
|
20966
|
+
this.db.prepare(
|
|
20967
|
+
`SELECT count(*) AS packs,
|
|
20609
20968
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
20610
20969
|
coalesce(sum(enabled), 0) AS enabled
|
|
20611
20970
|
FROM installed_packs`
|
|
20612
|
-
|
|
20613
|
-
|
|
20971
|
+
)
|
|
20972
|
+
);
|
|
20973
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
20614
20974
|
}
|
|
20615
20975
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
20616
20976
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -20623,26 +20983,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20623
20983
|
* attributed to Monitor, matching the Detections views.
|
|
20624
20984
|
*/
|
|
20625
20985
|
countsByPolicyId() {
|
|
20626
|
-
|
|
20627
|
-
|
|
20986
|
+
return countBy(
|
|
20987
|
+
this.db,
|
|
20988
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
20628
20989
|
FROM installed_packs
|
|
20629
|
-
GROUP BY
|
|
20630
|
-
)
|
|
20631
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20990
|
+
GROUP BY k`
|
|
20991
|
+
);
|
|
20632
20992
|
}
|
|
20633
20993
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
20634
20994
|
listByPolicyId(policyId) {
|
|
20635
|
-
const rows =
|
|
20636
|
-
|
|
20995
|
+
const rows = allRows(
|
|
20996
|
+
this.db.prepare(
|
|
20997
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
20637
20998
|
FROM installed_packs
|
|
20638
20999
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
20639
21000
|
ORDER BY name ASC`
|
|
20640
|
-
|
|
21001
|
+
),
|
|
21002
|
+
[policyId]
|
|
21003
|
+
);
|
|
20641
21004
|
return rows.map((r) => ({
|
|
20642
21005
|
id: `${r.namespace}/${r.packId}`,
|
|
20643
21006
|
name: r.name,
|
|
20644
21007
|
ruleCount: parseRules(r.rulesJson).length,
|
|
20645
|
-
enabled: r.enabled
|
|
21008
|
+
enabled: intToBool(r.enabled)
|
|
20646
21009
|
}));
|
|
20647
21010
|
}
|
|
20648
21011
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -20671,14 +21034,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20671
21034
|
const res = this.db.prepare(
|
|
20672
21035
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
20673
21036
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
20674
|
-
).run({ enabled: enabled
|
|
21037
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
20675
21038
|
return Number(res.changes) > 0;
|
|
20676
21039
|
}
|
|
20677
21040
|
// Fingerprint of the recorded available mirror — compared against the
|
|
20678
21041
|
// incoming inventory's signature to skip the write entirely when the running
|
|
20679
21042
|
// binary's inventory hasn't changed since the last record.
|
|
20680
21043
|
storedSignature() {
|
|
20681
|
-
const rows = this.signatureStmt
|
|
21044
|
+
const rows = allRows(this.signatureStmt);
|
|
20682
21045
|
return inventorySignature(rows);
|
|
20683
21046
|
}
|
|
20684
21047
|
};
|
|
@@ -20706,42 +21069,48 @@ var SqliteInventoryRepository = class {
|
|
|
20706
21069
|
upsert(input, now = Date.now()) {
|
|
20707
21070
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
20708
21071
|
const row = toInventoryRow(input, id, now);
|
|
20709
|
-
this.upsertStmt.run(
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
21072
|
+
this.upsertStmt.run(
|
|
21073
|
+
bindParams({
|
|
21074
|
+
id: row.id,
|
|
21075
|
+
objectType: row.objectType,
|
|
21076
|
+
location: row.location,
|
|
21077
|
+
title: row.title,
|
|
21078
|
+
hostId: row.hostId,
|
|
21079
|
+
attributes: row.attributes,
|
|
21080
|
+
firstSeen: row.firstSeen,
|
|
21081
|
+
lastSeen: row.lastSeen
|
|
21082
|
+
})
|
|
21083
|
+
);
|
|
20719
21084
|
return id;
|
|
20720
21085
|
}
|
|
20721
21086
|
// The full row, for round-trip assertions.
|
|
20722
21087
|
findById(id) {
|
|
20723
|
-
|
|
20724
|
-
return row;
|
|
21088
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
20725
21089
|
}
|
|
20726
21090
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
20727
21091
|
// served from the object_type index, never from audit_events.
|
|
20728
21092
|
distinctTitles(objectType) {
|
|
20729
|
-
const rows =
|
|
20730
|
-
|
|
21093
|
+
const rows = allRows(
|
|
21094
|
+
this.db.prepare(
|
|
21095
|
+
`SELECT DISTINCT title FROM inventory
|
|
20731
21096
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
20732
21097
|
ORDER BY title`
|
|
20733
|
-
|
|
21098
|
+
),
|
|
21099
|
+
{ objectType }
|
|
21100
|
+
);
|
|
20734
21101
|
return rows.map((r) => r.title);
|
|
20735
21102
|
}
|
|
20736
21103
|
// Distinct host os_version values — a facet served from an inventory index
|
|
20737
21104
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
20738
21105
|
// QUERY PLAN).
|
|
20739
21106
|
osVersions() {
|
|
20740
|
-
const rows =
|
|
20741
|
-
|
|
21107
|
+
const rows = allRows(
|
|
21108
|
+
this.db.prepare(
|
|
21109
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
20742
21110
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
20743
21111
|
ORDER BY value`
|
|
20744
|
-
|
|
21112
|
+
)
|
|
21113
|
+
);
|
|
20745
21114
|
return rows.map((r) => r.value);
|
|
20746
21115
|
}
|
|
20747
21116
|
};
|
|
@@ -20761,14 +21130,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
20761
21130
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
20762
21131
|
findingsCount: 0
|
|
20763
21132
|
};
|
|
20764
|
-
function safeJson(s, fallback) {
|
|
20765
|
-
if (s == null) return fallback;
|
|
20766
|
-
try {
|
|
20767
|
-
return JSON.parse(s);
|
|
20768
|
-
} catch {
|
|
20769
|
-
return fallback;
|
|
20770
|
-
}
|
|
20771
|
-
}
|
|
20772
21133
|
function resolveHarnessId(attrs, row) {
|
|
20773
21134
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
20774
21135
|
return attrs.provider;
|
|
@@ -20964,30 +21325,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20964
21325
|
configRowsCache;
|
|
20965
21326
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20966
21327
|
getInventoryStats() {
|
|
20967
|
-
const
|
|
20968
|
-
|
|
20969
|
-
|
|
20970
|
-
|
|
20971
|
-
byType
|
|
20972
|
-
|
|
20973
|
-
|
|
20974
|
-
|
|
21328
|
+
const typeCounts = countBy(
|
|
21329
|
+
this.db,
|
|
21330
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
21331
|
+
);
|
|
21332
|
+
const byType = {
|
|
21333
|
+
project: 0,
|
|
21334
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
21335
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
21336
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
21337
|
+
config: typeCounts.get("config") ?? 0
|
|
21338
|
+
};
|
|
21339
|
+
byType.project = countScalar(
|
|
21340
|
+
this.db,
|
|
21341
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
21342
|
+
);
|
|
21343
|
+
const mcpTrustCounts = countBy(
|
|
21344
|
+
this.db,
|
|
21345
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20975
21346
|
FROM inventory_asset a
|
|
20976
21347
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20977
21348
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20978
21349
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20979
|
-
)
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
|
|
21350
|
+
);
|
|
21351
|
+
const mcpTrust = {
|
|
21352
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
21353
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
21354
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
21355
|
+
};
|
|
21356
|
+
const harnesses = countScalar(
|
|
21357
|
+
this.db,
|
|
20983
21358
|
`SELECT count(*) AS n FROM inventory
|
|
20984
21359
|
WHERE object_type = 'harness'
|
|
20985
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20986
|
-
|
|
20987
|
-
|
|
20988
|
-
const
|
|
21360
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
21361
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
21362
|
+
);
|
|
21363
|
+
const flaggedAssets = countScalar(
|
|
21364
|
+
this.db,
|
|
21365
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
21366
|
+
);
|
|
21367
|
+
const flaggedProjects = countScalar(
|
|
21368
|
+
this.db,
|
|
20989
21369
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20990
|
-
)
|
|
21370
|
+
);
|
|
20991
21371
|
const configRows = this.configAssetRows();
|
|
20992
21372
|
for (const r of configRows) {
|
|
20993
21373
|
byType[r.assetType] += 1;
|
|
@@ -21244,12 +21624,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21244
21624
|
}
|
|
21245
21625
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
21246
21626
|
fetchHarnessRows() {
|
|
21247
|
-
return
|
|
21248
|
-
|
|
21627
|
+
return allRows(
|
|
21628
|
+
this.db.prepare(
|
|
21629
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
21249
21630
|
FROM inventory
|
|
21250
21631
|
WHERE object_type = 'harness'
|
|
21251
21632
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
21252
|
-
|
|
21633
|
+
),
|
|
21634
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
21635
|
+
);
|
|
21253
21636
|
}
|
|
21254
21637
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
21255
21638
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -21259,12 +21642,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21259
21642
|
const params = [...harnessInvIds];
|
|
21260
21643
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
21261
21644
|
if (q) {
|
|
21262
|
-
const pat =
|
|
21263
|
-
where +=
|
|
21645
|
+
const pat = containsPattern(q);
|
|
21646
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
21264
21647
|
params.push(pat, pat);
|
|
21265
21648
|
}
|
|
21266
|
-
const rows =
|
|
21267
|
-
|
|
21649
|
+
const rows = allRows(
|
|
21650
|
+
this.db.prepare(
|
|
21651
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
21268
21652
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
21269
21653
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
21270
21654
|
FROM harness_asset ha
|
|
@@ -21272,7 +21656,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21272
21656
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
21273
21657
|
WHERE ${where}
|
|
21274
21658
|
ORDER BY a.name ASC`
|
|
21275
|
-
|
|
21659
|
+
),
|
|
21660
|
+
params
|
|
21661
|
+
);
|
|
21276
21662
|
for (const raw of rows) {
|
|
21277
21663
|
const harnessInvId = raw.harnessInvId;
|
|
21278
21664
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -21291,21 +21677,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21291
21677
|
params.push(...types);
|
|
21292
21678
|
}
|
|
21293
21679
|
if (q) {
|
|
21294
|
-
const pat =
|
|
21295
|
-
conditions.push("
|
|
21680
|
+
const pat = containsPattern(q);
|
|
21681
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
21296
21682
|
params.push(pat, pat);
|
|
21297
21683
|
}
|
|
21298
21684
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
21299
21685
|
const sampleRows = this.mapAssetRows(
|
|
21300
|
-
|
|
21301
|
-
|
|
21686
|
+
allRows(
|
|
21687
|
+
this.db.prepare(
|
|
21688
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
21302
21689
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
21303
21690
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
21304
21691
|
FROM inventory_asset a
|
|
21305
21692
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
21306
21693
|
${where}
|
|
21307
21694
|
ORDER BY a.name ASC`
|
|
21308
|
-
|
|
21695
|
+
),
|
|
21696
|
+
params
|
|
21697
|
+
)
|
|
21309
21698
|
);
|
|
21310
21699
|
const configRows = this.configAssetRows(q).filter(
|
|
21311
21700
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -21314,14 +21703,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21314
21703
|
}
|
|
21315
21704
|
fetchAssetById(assetId) {
|
|
21316
21705
|
const rows = this.mapAssetRows(
|
|
21317
|
-
|
|
21318
|
-
|
|
21706
|
+
allRows(
|
|
21707
|
+
this.db.prepare(
|
|
21708
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
21319
21709
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
21320
21710
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
21321
21711
|
FROM inventory_asset a
|
|
21322
21712
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
21323
21713
|
WHERE a.id = ?`
|
|
21324
|
-
|
|
21714
|
+
),
|
|
21715
|
+
[assetId]
|
|
21716
|
+
)
|
|
21325
21717
|
);
|
|
21326
21718
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
21327
21719
|
}
|
|
@@ -21377,37 +21769,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21377
21769
|
return rows;
|
|
21378
21770
|
}
|
|
21379
21771
|
latestConfigScanId() {
|
|
21380
|
-
|
|
21381
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
21382
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
21383
|
-
).get();
|
|
21384
|
-
return row?.id ?? null;
|
|
21772
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
21385
21773
|
}
|
|
21386
21774
|
fetchProjects(q) {
|
|
21387
21775
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
21388
21776
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
21389
21777
|
const params = [];
|
|
21390
21778
|
if (q) {
|
|
21391
|
-
const pat =
|
|
21392
|
-
sql +=
|
|
21779
|
+
const pat = containsPattern(q);
|
|
21780
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
21393
21781
|
params.push(pat, pat);
|
|
21394
21782
|
}
|
|
21395
21783
|
sql += " ORDER BY name ASC";
|
|
21396
|
-
return this.db.prepare(sql)
|
|
21784
|
+
return allRows(this.db.prepare(sql), params);
|
|
21397
21785
|
}
|
|
21398
21786
|
fetchProjectById(projectId) {
|
|
21399
|
-
return
|
|
21400
|
-
|
|
21401
|
-
|
|
21787
|
+
return getRow(
|
|
21788
|
+
this.db.prepare(
|
|
21789
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
21790
|
+
),
|
|
21791
|
+
[projectId]
|
|
21792
|
+
) ?? null;
|
|
21402
21793
|
}
|
|
21403
21794
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
21404
21795
|
fetchProjectsByIds(projectIds) {
|
|
21405
21796
|
const map2 = /* @__PURE__ */ new Map();
|
|
21406
21797
|
if (projectIds.length === 0) return map2;
|
|
21407
|
-
const rows =
|
|
21408
|
-
|
|
21798
|
+
const rows = allRows(
|
|
21799
|
+
this.db.prepare(
|
|
21800
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
21409
21801
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
21410
|
-
|
|
21802
|
+
),
|
|
21803
|
+
projectIds
|
|
21804
|
+
);
|
|
21411
21805
|
for (const r of rows) map2.set(r.id, r);
|
|
21412
21806
|
return map2;
|
|
21413
21807
|
}
|
|
@@ -21418,8 +21812,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21418
21812
|
projectAggregates(projectIds) {
|
|
21419
21813
|
const map2 = /* @__PURE__ */ new Map();
|
|
21420
21814
|
if (projectIds.length === 0) return map2;
|
|
21421
|
-
const rows =
|
|
21422
|
-
|
|
21815
|
+
const rows = allRows(
|
|
21816
|
+
this.db.prepare(
|
|
21817
|
+
`SELECT f.project_id AS projectId,
|
|
21423
21818
|
coalesce(o.access, f.default_access) AS eff,
|
|
21424
21819
|
count(*) AS n,
|
|
21425
21820
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -21427,7 +21822,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21427
21822
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
21428
21823
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
21429
21824
|
GROUP BY f.project_id, eff`
|
|
21430
|
-
|
|
21825
|
+
),
|
|
21826
|
+
projectIds
|
|
21827
|
+
);
|
|
21431
21828
|
for (const r of rows) {
|
|
21432
21829
|
let agg = map2.get(r.projectId);
|
|
21433
21830
|
if (!agg) {
|
|
@@ -21465,37 +21862,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21465
21862
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
21466
21863
|
if (prefix === "") {
|
|
21467
21864
|
return this.mapFileRows(
|
|
21468
|
-
|
|
21865
|
+
allRows(
|
|
21866
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21867
|
+
[projectId]
|
|
21868
|
+
)
|
|
21469
21869
|
);
|
|
21470
21870
|
}
|
|
21471
21871
|
return this.mapFileRows(
|
|
21472
|
-
|
|
21473
|
-
this.
|
|
21474
|
-
|
|
21872
|
+
allRows(
|
|
21873
|
+
this.db.prepare(
|
|
21874
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21875
|
+
),
|
|
21876
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21877
|
+
)
|
|
21475
21878
|
);
|
|
21476
21879
|
}
|
|
21477
21880
|
fetchProjectFilesSearch(projectId, q) {
|
|
21478
|
-
const pat =
|
|
21881
|
+
const pat = containsPattern(q);
|
|
21479
21882
|
return this.mapFileRows(
|
|
21480
|
-
|
|
21481
|
-
this.
|
|
21482
|
-
|
|
21483
|
-
|
|
21484
|
-
|
|
21883
|
+
allRows(
|
|
21884
|
+
this.db.prepare(
|
|
21885
|
+
this.fileSelect(
|
|
21886
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21887
|
+
)
|
|
21888
|
+
),
|
|
21889
|
+
[projectId, pat, pat]
|
|
21890
|
+
)
|
|
21485
21891
|
);
|
|
21486
21892
|
}
|
|
21487
21893
|
fetchProjectFilesBlocked(projectId) {
|
|
21488
21894
|
return this.mapFileRows(
|
|
21489
|
-
|
|
21490
|
-
this.
|
|
21491
|
-
|
|
21492
|
-
|
|
21493
|
-
|
|
21895
|
+
allRows(
|
|
21896
|
+
this.db.prepare(
|
|
21897
|
+
this.fileSelect(
|
|
21898
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21899
|
+
)
|
|
21900
|
+
),
|
|
21901
|
+
[projectId]
|
|
21902
|
+
)
|
|
21494
21903
|
);
|
|
21495
21904
|
}
|
|
21496
21905
|
fetchProjectFile(projectId, path) {
|
|
21497
21906
|
const rows = this.mapFileRows(
|
|
21498
|
-
|
|
21907
|
+
allRows(
|
|
21908
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21909
|
+
[projectId, path]
|
|
21910
|
+
)
|
|
21499
21911
|
);
|
|
21500
21912
|
return rows[0] ?? null;
|
|
21501
21913
|
}
|
|
@@ -21509,39 +21921,32 @@ var SqlitePoliciesRepository = class {
|
|
|
21509
21921
|
}
|
|
21510
21922
|
db;
|
|
21511
21923
|
readPolicies() {
|
|
21512
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
21513
|
-
const policies =
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
|
|
21522
|
-
|
|
21523
|
-
|
|
21524
|
-
|
|
21525
|
-
customKeywords
|
|
21526
|
-
})
|
|
21527
|
-
);
|
|
21528
|
-
} catch {
|
|
21529
|
-
}
|
|
21530
|
-
}
|
|
21924
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21925
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21926
|
+
const target = JSON.parse(row.target);
|
|
21927
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21928
|
+
return Policy.parse({
|
|
21929
|
+
id: row.id,
|
|
21930
|
+
scope: row.scope,
|
|
21931
|
+
target,
|
|
21932
|
+
action: row.action,
|
|
21933
|
+
enabled: intToBool(row.enabled),
|
|
21934
|
+
customKeywords
|
|
21935
|
+
});
|
|
21936
|
+
});
|
|
21531
21937
|
return Promise.resolve(policies);
|
|
21532
21938
|
}
|
|
21533
21939
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
21534
21940
|
// detection-type config exists from first run. Only when the table is empty,
|
|
21535
21941
|
// so a user's edits are never clobbered.
|
|
21536
21942
|
seedDefaults() {
|
|
21537
|
-
const count = this.db
|
|
21943
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
21538
21944
|
if (count > 0) return;
|
|
21539
21945
|
const stmt = this.db.prepare(
|
|
21540
21946
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21541
21947
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
21542
21948
|
);
|
|
21543
|
-
this.db
|
|
21544
|
-
try {
|
|
21949
|
+
failOpenTransaction(this.db, () => {
|
|
21545
21950
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
21546
21951
|
stmt.run({
|
|
21547
21952
|
id: randomUUID5(),
|
|
@@ -21550,10 +21955,41 @@ var SqlitePoliciesRepository = class {
|
|
|
21550
21955
|
now: Date.now()
|
|
21551
21956
|
});
|
|
21552
21957
|
}
|
|
21553
|
-
|
|
21554
|
-
|
|
21555
|
-
|
|
21556
|
-
|
|
21958
|
+
});
|
|
21959
|
+
}
|
|
21960
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21961
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21962
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21963
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21964
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21965
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21966
|
+
upsertCategoryAction(category, action) {
|
|
21967
|
+
const now = Date.now();
|
|
21968
|
+
this.db.prepare(
|
|
21969
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21970
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21971
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21972
|
+
).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
|
|
21973
|
+
}
|
|
21974
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21975
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21976
|
+
// Returns the number of rows changed.
|
|
21977
|
+
capCategoryActions() {
|
|
21978
|
+
const info = this.db.prepare(
|
|
21979
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21980
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21981
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21982
|
+
).run({ now: Date.now() });
|
|
21983
|
+
return Number(info.changes);
|
|
21984
|
+
}
|
|
21985
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21986
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21987
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21988
|
+
getCategoryAction(category) {
|
|
21989
|
+
const row = this.db.prepare(
|
|
21990
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21991
|
+
).get({ category });
|
|
21992
|
+
return row?.action;
|
|
21557
21993
|
}
|
|
21558
21994
|
};
|
|
21559
21995
|
|
|
@@ -21629,7 +22065,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
21629
22065
|
maxStampStmt;
|
|
21630
22066
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
21631
22067
|
replaceForProject(projectId, scan2, now) {
|
|
21632
|
-
const
|
|
22068
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
21633
22069
|
const stamp = Math.max(now, maxStamp + 1);
|
|
21634
22070
|
for (const file2 of scan2.files) {
|
|
21635
22071
|
this.upsertStmt.run({
|
|
@@ -21712,7 +22148,7 @@ var SqliteResolutionsRepository = class {
|
|
|
21712
22148
|
}
|
|
21713
22149
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
21714
22150
|
latestByKey(key) {
|
|
21715
|
-
const row = this.latestStmt
|
|
22151
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
21716
22152
|
if (!row) return void 0;
|
|
21717
22153
|
return {
|
|
21718
22154
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -21729,7 +22165,7 @@ var SqliteResolutionsRepository = class {
|
|
|
21729
22165
|
* the CLI) surfaces for that file.
|
|
21730
22166
|
*/
|
|
21731
22167
|
openAtRestKeysForPath(path) {
|
|
21732
|
-
const rows = this.openAtRestStmt
|
|
22168
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
21733
22169
|
return rows.map((r) => r.finding_key);
|
|
21734
22170
|
}
|
|
21735
22171
|
/**
|
|
@@ -21740,7 +22176,7 @@ var SqliteResolutionsRepository = class {
|
|
|
21740
22176
|
* resolution row (see scan.ts).
|
|
21741
22177
|
*/
|
|
21742
22178
|
resolvedAtRestKeysForPath(path) {
|
|
21743
|
-
const rows = this.resolvedAtRestStmt
|
|
22179
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
21744
22180
|
return rows.map((r) => r.finding_key);
|
|
21745
22181
|
}
|
|
21746
22182
|
};
|
|
@@ -21769,31 +22205,25 @@ var SqliteScanLedgerRepository = class {
|
|
|
21769
22205
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
21770
22206
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
21771
22207
|
entriesForRuleset(rulesetHash) {
|
|
21772
|
-
const rows = this.readStmt
|
|
22208
|
+
const rows = allRows(this.readStmt, {
|
|
22209
|
+
rulesetHash
|
|
22210
|
+
});
|
|
21773
22211
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
21774
22212
|
}
|
|
21775
22213
|
upsertEntries(entries) {
|
|
21776
22214
|
if (entries.length === 0) return;
|
|
21777
22215
|
const scannedAt = Date.now();
|
|
21778
|
-
|
|
21779
|
-
|
|
21780
|
-
|
|
21781
|
-
|
|
21782
|
-
|
|
21783
|
-
|
|
21784
|
-
|
|
21785
|
-
|
|
21786
|
-
|
|
21787
|
-
scannedAt
|
|
21788
|
-
});
|
|
21789
|
-
}
|
|
21790
|
-
this.db.exec("COMMIT");
|
|
21791
|
-
} catch (err) {
|
|
21792
|
-
this.db.exec("ROLLBACK");
|
|
21793
|
-
throw err;
|
|
22216
|
+
failOpenTransaction(this.db, () => {
|
|
22217
|
+
for (const entry of entries) {
|
|
22218
|
+
this.upsertStmt.run({
|
|
22219
|
+
path: entry.path,
|
|
22220
|
+
mtime: entry.mtime,
|
|
22221
|
+
contentHash: entry.contentHash,
|
|
22222
|
+
rulesetHash: entry.rulesetHash,
|
|
22223
|
+
scannedAt
|
|
22224
|
+
});
|
|
21794
22225
|
}
|
|
21795
|
-
}
|
|
21796
|
-
}
|
|
22226
|
+
});
|
|
21797
22227
|
}
|
|
21798
22228
|
};
|
|
21799
22229
|
|
|
@@ -21870,8 +22300,9 @@ var SqliteSecurityRepository = class {
|
|
|
21870
22300
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21871
22301
|
// double-counting a key that accumulated several append-only rows.
|
|
21872
22302
|
severitySummary() {
|
|
21873
|
-
const rows =
|
|
21874
|
-
|
|
22303
|
+
const rows = allRows(
|
|
22304
|
+
this.db.prepare(
|
|
22305
|
+
`SELECT f.severity AS severity,
|
|
21875
22306
|
COUNT(*) AS count,
|
|
21876
22307
|
SUM(CASE
|
|
21877
22308
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21890,7 +22321,8 @@ var SqliteSecurityRepository = class {
|
|
|
21890
22321
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21891
22322
|
ON latest.finding_key = f.finding_key
|
|
21892
22323
|
GROUP BY f.severity`
|
|
21893
|
-
|
|
22324
|
+
)
|
|
22325
|
+
);
|
|
21894
22326
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21895
22327
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21896
22328
|
severity,
|
|
@@ -21974,14 +22406,15 @@ var SqliteSecurityRepository = class {
|
|
|
21974
22406
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21975
22407
|
const now = this.now();
|
|
21976
22408
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21977
|
-
const rows =
|
|
21978
|
-
|
|
21979
|
-
|
|
21980
|
-
|
|
21981
|
-
|
|
21982
|
-
|
|
21983
|
-
|
|
21984
|
-
|
|
22409
|
+
const rows = allRows(
|
|
22410
|
+
this.db.prepare(
|
|
22411
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
22412
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
22413
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
22414
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
22415
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
22416
|
+
// backfill left null.
|
|
22417
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21985
22418
|
(
|
|
21986
22419
|
SELECT fr.status FROM finding_resolution fr
|
|
21987
22420
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22007,14 +22440,16 @@ var SqliteSecurityRepository = class {
|
|
|
22007
22440
|
WHERE fr.finding_key = f.finding_key
|
|
22008
22441
|
AND fr.resolved_at >= :windowStart
|
|
22009
22442
|
)`
|
|
22010
|
-
|
|
22011
|
-
|
|
22012
|
-
|
|
22013
|
-
|
|
22014
|
-
|
|
22015
|
-
|
|
22016
|
-
|
|
22017
|
-
|
|
22443
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
22444
|
+
// any resolution activity at/after the window start — a row this method
|
|
22445
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
22446
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
22447
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
22448
|
+
// dialect-agnostic. Without this, a
|
|
22449
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
22450
|
+
),
|
|
22451
|
+
{ windowStart }
|
|
22452
|
+
);
|
|
22018
22453
|
const sums = /* @__PURE__ */ new Map();
|
|
22019
22454
|
const counts = /* @__PURE__ */ new Map();
|
|
22020
22455
|
for (const r of rows) {
|
|
@@ -22044,8 +22479,9 @@ var SqliteSecurityRepository = class {
|
|
|
22044
22479
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
22045
22480
|
const now = this.now();
|
|
22046
22481
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22047
|
-
const rows =
|
|
22048
|
-
|
|
22482
|
+
const rows = allRows(
|
|
22483
|
+
this.db.prepare(
|
|
22484
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
22049
22485
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22050
22486
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
22051
22487
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -22053,7 +22489,9 @@ var SqliteSecurityRepository = class {
|
|
|
22053
22489
|
GROUP BY repo
|
|
22054
22490
|
ORDER BY c DESC, repo
|
|
22055
22491
|
LIMIT :limit`
|
|
22056
|
-
|
|
22492
|
+
),
|
|
22493
|
+
{ from, to: now, limit }
|
|
22494
|
+
);
|
|
22057
22495
|
const items = rows.map((r) => ({
|
|
22058
22496
|
id: `repo_${r.repo}`,
|
|
22059
22497
|
name: r.repo,
|
|
@@ -22075,8 +22513,9 @@ var SqliteSecurityRepository = class {
|
|
|
22075
22513
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
22076
22514
|
// capped at `limit`.
|
|
22077
22515
|
recentlyResolved(limit = 20) {
|
|
22078
|
-
const rows =
|
|
22079
|
-
|
|
22516
|
+
const rows = allRows(
|
|
22517
|
+
this.db.prepare(
|
|
22518
|
+
`SELECT f.finding_key AS finding_key,
|
|
22080
22519
|
f.rule_id AS rule_id,
|
|
22081
22520
|
f.severity AS severity,
|
|
22082
22521
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -22110,7 +22549,9 @@ var SqliteSecurityRepository = class {
|
|
|
22110
22549
|
) IS NOT NULL
|
|
22111
22550
|
ORDER BY latest_resolved_at DESC
|
|
22112
22551
|
LIMIT :limit`
|
|
22113
|
-
|
|
22552
|
+
),
|
|
22553
|
+
{ limit }
|
|
22554
|
+
);
|
|
22114
22555
|
const items = rows.map((r) => ({
|
|
22115
22556
|
findingKey: r.finding_key,
|
|
22116
22557
|
ruleId: r.rule_id,
|
|
@@ -22127,12 +22568,15 @@ var SqliteSecurityRepository = class {
|
|
|
22127
22568
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
22128
22569
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22129
22570
|
findingsInRange(fromMs, toMs) {
|
|
22130
|
-
const rows =
|
|
22131
|
-
|
|
22571
|
+
const rows = allRows(
|
|
22572
|
+
this.db.prepare(
|
|
22573
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
22132
22574
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22133
22575
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
22134
22576
|
ORDER BY e.occurred_at`
|
|
22135
|
-
|
|
22577
|
+
),
|
|
22578
|
+
{ from: fromMs, to: toMs }
|
|
22579
|
+
);
|
|
22136
22580
|
return rows.map((r) => ({
|
|
22137
22581
|
occurredAt: r.occurred_at,
|
|
22138
22582
|
severity: r.severity,
|
|
@@ -22146,12 +22590,7 @@ import { randomUUID as randomUUID8 } from "crypto";
|
|
|
22146
22590
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
22147
22591
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22148
22592
|
function parseNetwork(networkJson) {
|
|
22149
|
-
|
|
22150
|
-
try {
|
|
22151
|
-
return JSON.parse(networkJson);
|
|
22152
|
-
} catch {
|
|
22153
|
-
return null;
|
|
22154
|
-
}
|
|
22593
|
+
return safeJson(networkJson, null);
|
|
22155
22594
|
}
|
|
22156
22595
|
function toEndpointSummary(row) {
|
|
22157
22596
|
return {
|
|
@@ -22238,27 +22677,39 @@ var SqliteSharesRepository = class {
|
|
|
22238
22677
|
}
|
|
22239
22678
|
db;
|
|
22240
22679
|
stats() {
|
|
22241
|
-
const
|
|
22242
|
-
const
|
|
22243
|
-
const
|
|
22244
|
-
const
|
|
22245
|
-
|
|
22680
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
22681
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
22682
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22683
|
+
const insecure = countScalar(
|
|
22684
|
+
this.db,
|
|
22246
22685
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
22247
22686
|
);
|
|
22248
|
-
const needsReview =
|
|
22687
|
+
const needsReview = countScalar(
|
|
22688
|
+
this.db,
|
|
22249
22689
|
`SELECT count(DISTINCT d.id) AS n
|
|
22250
22690
|
FROM share_destination d
|
|
22251
22691
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
22252
22692
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22253
22693
|
);
|
|
22254
|
-
const
|
|
22255
|
-
|
|
22256
|
-
|
|
22257
|
-
|
|
22258
|
-
const
|
|
22259
|
-
|
|
22260
|
-
|
|
22261
|
-
|
|
22694
|
+
const kindCounts = countBy(
|
|
22695
|
+
this.db,
|
|
22696
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
22697
|
+
);
|
|
22698
|
+
const byKind = {
|
|
22699
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
22700
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
22701
|
+
ip: kindCounts.get("ip") ?? 0
|
|
22702
|
+
};
|
|
22703
|
+
const trustCounts = countBy(
|
|
22704
|
+
this.db,
|
|
22705
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
22706
|
+
);
|
|
22707
|
+
const byTrust = {
|
|
22708
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
22709
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
22710
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
22711
|
+
ip: trustCounts.get("ip") ?? 0
|
|
22712
|
+
};
|
|
22262
22713
|
return Promise.resolve({
|
|
22263
22714
|
destinations,
|
|
22264
22715
|
endpoints,
|
|
@@ -22374,7 +22825,7 @@ var SqliteSharesRepository = class {
|
|
|
22374
22825
|
}
|
|
22375
22826
|
let sql;
|
|
22376
22827
|
if (q) {
|
|
22377
|
-
const pattern =
|
|
22828
|
+
const pattern = containsPattern(q);
|
|
22378
22829
|
conditions.push(
|
|
22379
22830
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
22380
22831
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -22394,24 +22845,31 @@ var SqliteSharesRepository = class {
|
|
|
22394
22845
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22395
22846
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22396
22847
|
}
|
|
22397
|
-
const rows =
|
|
22848
|
+
const rows = allRows(
|
|
22849
|
+
this.db.prepare(sql),
|
|
22850
|
+
params
|
|
22851
|
+
);
|
|
22398
22852
|
return rows.map((r) => this.mapDestRow(r));
|
|
22399
22853
|
}
|
|
22400
22854
|
fetchDestinationById(destinationId) {
|
|
22401
|
-
const row =
|
|
22402
|
-
|
|
22855
|
+
const row = getRow(
|
|
22856
|
+
this.db.prepare(
|
|
22857
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22403
22858
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22404
22859
|
o.decision AS overrideDecision
|
|
22405
22860
|
FROM share_destination d
|
|
22406
22861
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
22407
22862
|
WHERE d.id = ?`
|
|
22408
|
-
|
|
22863
|
+
),
|
|
22864
|
+
[destinationId]
|
|
22865
|
+
);
|
|
22409
22866
|
return row ? this.mapDestRow(row) : null;
|
|
22410
22867
|
}
|
|
22411
22868
|
fetchEndpoints(destinationIds) {
|
|
22412
22869
|
if (destinationIds.length === 0) return [];
|
|
22413
|
-
const rows =
|
|
22414
|
-
|
|
22870
|
+
const rows = allRows(
|
|
22871
|
+
this.db.prepare(
|
|
22872
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
22415
22873
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
22416
22874
|
count(c.id) AS callSiteCount
|
|
22417
22875
|
FROM share_endpoint e
|
|
@@ -22419,7 +22877,9 @@ var SqliteSharesRepository = class {
|
|
|
22419
22877
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
22420
22878
|
GROUP BY e.id
|
|
22421
22879
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
22422
|
-
|
|
22880
|
+
),
|
|
22881
|
+
destinationIds
|
|
22882
|
+
);
|
|
22423
22883
|
return rows.map((r) => ({
|
|
22424
22884
|
id: r.id,
|
|
22425
22885
|
destinationId: r.destinationId,
|
|
@@ -22444,13 +22904,16 @@ var SqliteSharesRepository = class {
|
|
|
22444
22904
|
}
|
|
22445
22905
|
fetchCallSites(endpointIds) {
|
|
22446
22906
|
if (endpointIds.length === 0) return [];
|
|
22447
|
-
const rows =
|
|
22448
|
-
|
|
22907
|
+
const rows = allRows(
|
|
22908
|
+
this.db.prepare(
|
|
22909
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
22449
22910
|
project_id AS projectId
|
|
22450
22911
|
FROM share_call_site
|
|
22451
22912
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
22452
22913
|
ORDER BY created_at ASC, id ASC`
|
|
22453
|
-
|
|
22914
|
+
),
|
|
22915
|
+
endpointIds
|
|
22916
|
+
);
|
|
22454
22917
|
return rows.map((r) => ({
|
|
22455
22918
|
id: r.id,
|
|
22456
22919
|
endpointId: r.endpointId,
|
|
@@ -22486,27 +22949,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
22486
22949
|
upsert(input, now = Date.now()) {
|
|
22487
22950
|
const id = sourceProjectId(input.url);
|
|
22488
22951
|
const row = toSourceProjectRow(input, id, now);
|
|
22489
|
-
this.upsertStmt.run(
|
|
22490
|
-
|
|
22491
|
-
|
|
22492
|
-
|
|
22493
|
-
|
|
22494
|
-
|
|
22495
|
-
|
|
22496
|
-
|
|
22952
|
+
this.upsertStmt.run(
|
|
22953
|
+
bindParams({
|
|
22954
|
+
id: row.id,
|
|
22955
|
+
url: row.url,
|
|
22956
|
+
name: row.name,
|
|
22957
|
+
attributes: row.attributes,
|
|
22958
|
+
firstSeen: row.firstSeen,
|
|
22959
|
+
lastSeen: row.lastSeen
|
|
22960
|
+
})
|
|
22961
|
+
);
|
|
22497
22962
|
return id;
|
|
22498
22963
|
}
|
|
22499
22964
|
findById(id) {
|
|
22500
|
-
return
|
|
22965
|
+
return getRow(
|
|
22966
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22967
|
+
{
|
|
22968
|
+
id
|
|
22969
|
+
}
|
|
22970
|
+
);
|
|
22501
22971
|
}
|
|
22502
22972
|
// Distinct project names — a filter facet, served from the source_project
|
|
22503
22973
|
// table, never from the audit fact table.
|
|
22504
22974
|
distinctNames() {
|
|
22505
|
-
const rows =
|
|
22506
|
-
|
|
22975
|
+
const rows = allRows(
|
|
22976
|
+
this.db.prepare(
|
|
22977
|
+
`SELECT DISTINCT name FROM source_project
|
|
22507
22978
|
WHERE name IS NOT NULL
|
|
22508
22979
|
ORDER BY name`
|
|
22509
|
-
|
|
22980
|
+
)
|
|
22981
|
+
);
|
|
22510
22982
|
return rows.map((r) => r.name);
|
|
22511
22983
|
}
|
|
22512
22984
|
};
|
|
@@ -22525,8 +22997,7 @@ function hasLegacySampleRows(db) {
|
|
|
22525
22997
|
function purgeSampleData(db) {
|
|
22526
22998
|
try {
|
|
22527
22999
|
if (!hasLegacySampleRows(db)) return;
|
|
22528
|
-
db
|
|
22529
|
-
try {
|
|
23000
|
+
withTransaction(db, () => {
|
|
22530
23001
|
db.exec(
|
|
22531
23002
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
22532
23003
|
SELECT e.id FROM share_endpoint e
|
|
@@ -22571,11 +23042,7 @@ function purgeSampleData(db) {
|
|
|
22571
23042
|
value TEXT NOT NULL
|
|
22572
23043
|
)`);
|
|
22573
23044
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
22574
|
-
|
|
22575
|
-
} catch (err) {
|
|
22576
|
-
db.exec("ROLLBACK");
|
|
22577
|
-
throw err;
|
|
22578
|
-
}
|
|
23045
|
+
});
|
|
22579
23046
|
} catch {
|
|
22580
23047
|
}
|
|
22581
23048
|
}
|
|
@@ -22594,7 +23061,7 @@ function openWithPragmas(file2) {
|
|
|
22594
23061
|
function backupLegacyStore(file2) {
|
|
22595
23062
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22596
23063
|
renameSync(file2, backup);
|
|
22597
|
-
for (const sidecar of
|
|
23064
|
+
for (const sidecar of walSidecars(file2)) {
|
|
22598
23065
|
if (existsSync2(sidecar)) rmSync(sidecar);
|
|
22599
23066
|
}
|
|
22600
23067
|
return backup;
|
|
@@ -22607,9 +23074,8 @@ function openLocalDatabase(dir) {
|
|
|
22607
23074
|
db.close();
|
|
22608
23075
|
const backup = backupLegacyStore(file2);
|
|
22609
23076
|
db = openWithPragmas(file2);
|
|
22610
|
-
|
|
22611
|
-
`
|
|
22612
|
-
`
|
|
23077
|
+
akaWarn(
|
|
23078
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22613
23079
|
);
|
|
22614
23080
|
}
|
|
22615
23081
|
applyMigrations(db);
|
|
@@ -22637,96 +23103,77 @@ function openLocalDatabase(dir) {
|
|
|
22637
23103
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
22638
23104
|
policies.seedDefaults();
|
|
22639
23105
|
function recordCapture(event, detected) {
|
|
22640
|
-
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
|
|
22645
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22646
|
-
db.exec("COMMIT");
|
|
22647
|
-
} catch (err) {
|
|
22648
|
-
db.exec("ROLLBACK");
|
|
22649
|
-
throw err;
|
|
22650
|
-
}
|
|
22651
|
-
} catch {
|
|
22652
|
-
}
|
|
23106
|
+
failOpenTransaction(db, () => {
|
|
23107
|
+
events.insertEvent(event);
|
|
23108
|
+
const sessionId = event.metadata?.sessionId;
|
|
23109
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
23110
|
+
});
|
|
22653
23111
|
}
|
|
22654
23112
|
function ensureInventory(ctx) {
|
|
22655
23113
|
const resolved = {};
|
|
22656
|
-
|
|
22657
|
-
|
|
22658
|
-
|
|
22659
|
-
|
|
22660
|
-
|
|
22661
|
-
|
|
22662
|
-
|
|
22663
|
-
|
|
22664
|
-
|
|
22665
|
-
|
|
22666
|
-
|
|
22667
|
-
|
|
22668
|
-
|
|
22669
|
-
|
|
22670
|
-
|
|
22671
|
-
|
|
22672
|
-
|
|
22673
|
-
|
|
22674
|
-
|
|
22675
|
-
|
|
22676
|
-
db.exec("COMMIT");
|
|
22677
|
-
} catch (err) {
|
|
22678
|
-
db.exec("ROLLBACK");
|
|
22679
|
-
throw err;
|
|
22680
|
-
}
|
|
22681
|
-
} catch {
|
|
22682
|
-
return {};
|
|
22683
|
-
}
|
|
22684
|
-
return resolved;
|
|
23114
|
+
const committed = failOpenTransaction(db, () => {
|
|
23115
|
+
const now = Date.now();
|
|
23116
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
23117
|
+
if (ctx.harness) {
|
|
23118
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
23119
|
+
}
|
|
23120
|
+
resolved.accountId = inventory.upsert(
|
|
23121
|
+
linkHost(
|
|
23122
|
+
{
|
|
23123
|
+
objectType: "user",
|
|
23124
|
+
identityKey: "local",
|
|
23125
|
+
attributes: { source: "local" }
|
|
23126
|
+
},
|
|
23127
|
+
resolved.hostId
|
|
23128
|
+
),
|
|
23129
|
+
now
|
|
23130
|
+
);
|
|
23131
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
23132
|
+
});
|
|
23133
|
+
return committed ? resolved : {};
|
|
22685
23134
|
}
|
|
22686
23135
|
function recordConfigScan(record2) {
|
|
22687
|
-
|
|
22688
|
-
|
|
22689
|
-
|
|
22690
|
-
|
|
22691
|
-
|
|
22692
|
-
|
|
22693
|
-
|
|
22694
|
-
|
|
22695
|
-
|
|
22696
|
-
}
|
|
22697
|
-
|
|
22698
|
-
|
|
22699
|
-
|
|
22700
|
-
|
|
22701
|
-
|
|
22702
|
-
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
|
|
22706
|
-
|
|
22707
|
-
confidence: finding.confidence
|
|
22708
|
-
});
|
|
22709
|
-
}
|
|
22710
|
-
db.exec("COMMIT");
|
|
22711
|
-
} catch (err) {
|
|
22712
|
-
db.exec("ROLLBACK");
|
|
22713
|
-
throw err;
|
|
23136
|
+
failOpenTransaction(db, () => {
|
|
23137
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
23138
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
23139
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
23140
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23141
|
+
for (const def of record2.definitions ?? []) {
|
|
23142
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
23143
|
+
}
|
|
23144
|
+
for (const finding of record2.findings ?? []) {
|
|
23145
|
+
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
23146
|
+
if (!definitionId) continue;
|
|
23147
|
+
inspectionFindings.insertFinding({
|
|
23148
|
+
id: randomUUID9(),
|
|
23149
|
+
auditEventId: record2.scanEvent.id,
|
|
23150
|
+
inspectionDefinitionId: definitionId,
|
|
23151
|
+
span: finding.span,
|
|
23152
|
+
maskedMatch: finding.maskedMatch,
|
|
23153
|
+
actionTaken: finding.actionTaken,
|
|
23154
|
+
confidence: finding.confidence
|
|
23155
|
+
});
|
|
22714
23156
|
}
|
|
22715
|
-
}
|
|
22716
|
-
}
|
|
23157
|
+
});
|
|
22717
23158
|
}
|
|
22718
23159
|
function recordProjectFiles(projectId, scan2) {
|
|
22719
23160
|
if (scan2.files.length === 0) return;
|
|
23161
|
+
failOpenTransaction(db, () => {
|
|
23162
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
23163
|
+
});
|
|
23164
|
+
}
|
|
23165
|
+
async function transaction(fn) {
|
|
23166
|
+
db.exec("BEGIN");
|
|
22720
23167
|
try {
|
|
22721
|
-
|
|
23168
|
+
const result = await fn();
|
|
23169
|
+
db.exec("COMMIT");
|
|
23170
|
+
return result;
|
|
23171
|
+
} catch (err) {
|
|
22722
23172
|
try {
|
|
22723
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22724
|
-
db.exec("COMMIT");
|
|
22725
|
-
} catch (err) {
|
|
22726
23173
|
db.exec("ROLLBACK");
|
|
22727
|
-
|
|
23174
|
+
} catch {
|
|
22728
23175
|
}
|
|
22729
|
-
|
|
23176
|
+
throw err;
|
|
22730
23177
|
}
|
|
22731
23178
|
}
|
|
22732
23179
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -22745,8 +23192,7 @@ function openLocalDatabase(dir) {
|
|
|
22745
23192
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
22746
23193
|
});
|
|
22747
23194
|
if (stale.length === 0) return;
|
|
22748
|
-
db
|
|
22749
|
-
try {
|
|
23195
|
+
withTransaction(db, () => {
|
|
22750
23196
|
for (const { id } of stale) {
|
|
22751
23197
|
db.prepare(
|
|
22752
23198
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -22758,11 +23204,7 @@ function openLocalDatabase(dir) {
|
|
|
22758
23204
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
22759
23205
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
22760
23206
|
}
|
|
22761
|
-
|
|
22762
|
-
} catch (err) {
|
|
22763
|
-
db.exec("ROLLBACK");
|
|
22764
|
-
throw err;
|
|
22765
|
-
}
|
|
23207
|
+
});
|
|
22766
23208
|
} catch {
|
|
22767
23209
|
}
|
|
22768
23210
|
}
|
|
@@ -22804,6 +23246,7 @@ function openLocalDatabase(dir) {
|
|
|
22804
23246
|
purgeSampleData: () => {
|
|
22805
23247
|
purgeSampleData(db);
|
|
22806
23248
|
},
|
|
23249
|
+
transaction,
|
|
22807
23250
|
close: () => {
|
|
22808
23251
|
db.close();
|
|
22809
23252
|
}
|
|
@@ -22908,190 +23351,9 @@ function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
|
22908
23351
|
import { readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
22909
23352
|
import { join as join6 } from "path";
|
|
22910
23353
|
|
|
22911
|
-
// ../packages/
|
|
22912
|
-
|
|
22913
|
-
|
|
22914
|
-
return join7(dataDir(home), "update-check.json");
|
|
22915
|
-
}
|
|
22916
|
-
function readCache(home) {
|
|
22917
|
-
const path = cachePath(home);
|
|
22918
|
-
if (!existsSync3(path)) return null;
|
|
22919
|
-
try {
|
|
22920
|
-
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
22921
|
-
if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
|
|
22922
|
-
return raw;
|
|
22923
|
-
} catch {
|
|
22924
|
-
return null;
|
|
22925
|
-
}
|
|
22926
|
-
}
|
|
22927
|
-
function writeCache(home, cache) {
|
|
22928
|
-
if (!existsSync3(dataDir(home))) return;
|
|
22929
|
-
try {
|
|
22930
|
-
writeFileSync3(cachePath(home), `${JSON.stringify(cache, null, 2)}
|
|
22931
|
-
`);
|
|
22932
|
-
} catch {
|
|
22933
|
-
}
|
|
22934
|
-
}
|
|
22935
|
-
function isStale(cache, ttlMs = CHECK_TTL_MS) {
|
|
22936
|
-
return Date.now() - cache.checkedAt >= ttlMs;
|
|
22937
|
-
}
|
|
22938
|
-
function clearCache(home) {
|
|
22939
|
-
try {
|
|
22940
|
-
rmSync2(cachePath(home), { force: true });
|
|
22941
|
-
} catch {
|
|
22942
|
-
}
|
|
22943
|
-
}
|
|
22944
|
-
function refreshCache(home) {
|
|
22945
|
-
const previous = readCache(home);
|
|
22946
|
-
writeCache(home, {
|
|
22947
|
-
checkedAt: Date.now(),
|
|
22948
|
-
report: gatherReportLive(),
|
|
22949
|
-
notifiedPluginIds: previous?.notifiedPluginIds ?? []
|
|
22950
|
-
});
|
|
22951
|
-
}
|
|
22952
|
-
function triggerBackgroundRefresh(home) {
|
|
22953
|
-
const entry = process.argv[1];
|
|
22954
|
-
if (!entry) return;
|
|
22955
|
-
try {
|
|
22956
|
-
spawn(process.execPath, [entry, "__update-refresh", "--home", home], {
|
|
22957
|
-
detached: true,
|
|
22958
|
-
stdio: "ignore"
|
|
22959
|
-
}).unref();
|
|
22960
|
-
} catch {
|
|
22961
|
-
}
|
|
22962
|
-
}
|
|
22963
|
-
function notifyFromCache(home, opts) {
|
|
22964
|
-
if (!opts.isTty) return;
|
|
22965
|
-
const cache = readCache(home);
|
|
22966
|
-
if (cache) {
|
|
22967
|
-
const lines = [];
|
|
22968
|
-
for (const s of cache.report.statuses) {
|
|
22969
|
-
if (!s.updateAvailable || !s.latest) continue;
|
|
22970
|
-
const cmd = s.kind === "cli" ? "aka update" : `aka update ${s.id}`;
|
|
22971
|
-
lines.push(` \u2B06 ${s.name}: ${s.installed ?? "\u2014"} \u2192 ${s.latest} available. Run \`${cmd}\`.`);
|
|
22972
|
-
}
|
|
22973
|
-
const alreadyShown = new Set(cache.notifiedPluginIds);
|
|
22974
|
-
const freshlyShown = [];
|
|
22975
|
-
for (const p of cache.report.availablePlugins) {
|
|
22976
|
-
if (alreadyShown.has(p.id)) continue;
|
|
22977
|
-
lines.push(
|
|
22978
|
-
` \u2728 New plugin available: ${p.name}. Install with \`aka plugins install ${p.id}\`.`
|
|
22979
|
-
);
|
|
22980
|
-
freshlyShown.push(p.id);
|
|
22981
|
-
}
|
|
22982
|
-
if (lines.length > 0) {
|
|
22983
|
-
process.stderr.write(`
|
|
22984
|
-
${lines.join("\n")}
|
|
22985
|
-
`);
|
|
22986
|
-
}
|
|
22987
|
-
if (freshlyShown.length > 0) {
|
|
22988
|
-
writeCache(home, {
|
|
22989
|
-
...cache,
|
|
22990
|
-
notifiedPluginIds: [...cache.notifiedPluginIds, ...freshlyShown]
|
|
22991
|
-
});
|
|
22992
|
-
}
|
|
22993
|
-
}
|
|
22994
|
-
if (!cache || isStale(cache)) triggerBackgroundRefresh(home);
|
|
22995
|
-
}
|
|
22996
|
-
|
|
22997
|
-
// ../packages/local-ops/src/update-render.ts
|
|
22998
|
-
function statusLabel(s) {
|
|
22999
|
-
if (s.installed === null || s.latest === null) return "unknown";
|
|
23000
|
-
return s.updateAvailable ? "update available" : "up to date";
|
|
23001
|
-
}
|
|
23002
|
-
function renderReport(report) {
|
|
23003
|
-
const rows = report.statuses.map((s) => ({
|
|
23004
|
-
name: s.name,
|
|
23005
|
-
installed: s.installed ?? "\u2014",
|
|
23006
|
-
latest: s.latest ?? "unknown",
|
|
23007
|
-
status: statusLabel(s)
|
|
23008
|
-
}));
|
|
23009
|
-
const nameW = Math.max(9, ...rows.map((r) => r.name.length));
|
|
23010
|
-
const instW = Math.max(9, ...rows.map((r) => r.installed.length));
|
|
23011
|
-
const latW = Math.max(6, ...rows.map((r) => r.latest.length));
|
|
23012
|
-
const lines = [];
|
|
23013
|
-
lines.push(
|
|
23014
|
-
` ${"Component".padEnd(nameW)} ${"Installed".padEnd(instW)} ${"Latest".padEnd(latW)} Status`
|
|
23015
|
-
);
|
|
23016
|
-
for (const r of rows) {
|
|
23017
|
-
lines.push(
|
|
23018
|
-
` ${r.name.padEnd(nameW)} ${r.installed.padEnd(instW)} ${r.latest.padEnd(latW)} ${r.status}`
|
|
23019
|
-
);
|
|
23020
|
-
}
|
|
23021
|
-
if (report.availablePlugins.length > 0) {
|
|
23022
|
-
lines.push("");
|
|
23023
|
-
lines.push(" Available plugins (not installed):");
|
|
23024
|
-
for (const p of report.availablePlugins) {
|
|
23025
|
-
const version2 = p.latest ? ` v${p.latest}` : "";
|
|
23026
|
-
lines.push(` ${p.name} (${p.id})${version2} \u2014 install: aka plugins install ${p.id}`);
|
|
23027
|
-
}
|
|
23028
|
-
}
|
|
23029
|
-
return lines.join("\n");
|
|
23030
|
-
}
|
|
23031
|
-
function outdated(report) {
|
|
23032
|
-
return report.statuses.filter((s) => s.updateAvailable);
|
|
23033
|
-
}
|
|
23034
|
-
|
|
23035
|
-
// src/command-manifest.ts
|
|
23036
|
-
var COMMAND_SPECS = [
|
|
23037
|
-
{ name: "init", summary: "Scaffold ~/.aka (settings + local database)" },
|
|
23038
|
-
{
|
|
23039
|
-
name: "scan",
|
|
23040
|
-
argHint: "[path]",
|
|
23041
|
-
summary: "Scan a file or directory and record findings (default: .)",
|
|
23042
|
-
completesFiles: true
|
|
23043
|
-
},
|
|
23044
|
-
{ name: "stats", summary: "Print findings / enforcement / detections from the local store" },
|
|
23045
|
-
{
|
|
23046
|
-
name: "detections",
|
|
23047
|
-
argHint: "[update]",
|
|
23048
|
-
summary: "List installed detection packs + available updates (updates are manual)",
|
|
23049
|
-
args: ["update"]
|
|
23050
|
-
},
|
|
23051
|
-
{
|
|
23052
|
-
name: "plugins",
|
|
23053
|
-
summary: "List / install agent plugins (the CLI is an optional hub)",
|
|
23054
|
-
args: ["list", "install"]
|
|
23055
|
-
},
|
|
23056
|
-
{ name: "dashboard", summary: "Launch the local web dashboard and open it in the browser" },
|
|
23057
|
-
{
|
|
23058
|
-
name: "exception",
|
|
23059
|
-
summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
|
|
23060
|
-
args: ["approve", "add", "list", "show", "revoke", "rotate-key"]
|
|
23061
|
-
},
|
|
23062
|
-
{
|
|
23063
|
-
name: "tui",
|
|
23064
|
-
argHint: "[view]",
|
|
23065
|
-
summary: "Interactive colour dashboard \u2014 health|findings|recommend|audit (needs a TTY)",
|
|
23066
|
-
args: ["health", "findings", "recommend", "audit"]
|
|
23067
|
-
},
|
|
23068
|
-
{
|
|
23069
|
-
name: "check-updates",
|
|
23070
|
-
summary: "Show which components (CLI + plugins) have updates available"
|
|
23071
|
-
},
|
|
23072
|
-
{
|
|
23073
|
-
name: "update",
|
|
23074
|
-
argHint: "[what]",
|
|
23075
|
-
summary: "Update the CLI and/or plugins (what: cli | <plugin-id> | all)",
|
|
23076
|
-
args: ["cli", "all"]
|
|
23077
|
-
},
|
|
23078
|
-
{
|
|
23079
|
-
name: "completion",
|
|
23080
|
-
argHint: "<sh>",
|
|
23081
|
-
summary: "Emit a shell completion script (sh: zsh | bash)",
|
|
23082
|
-
args: ["zsh", "bash"]
|
|
23083
|
-
}
|
|
23084
|
-
];
|
|
23085
|
-
var GLOBAL_FLAGS = ["--home", "--no-update-check", "--version", "--help"];
|
|
23086
|
-
function commandsHelp() {
|
|
23087
|
-
const label = (s) => s.argHint ? `${s.name} ${s.argHint}` : s.name;
|
|
23088
|
-
const width = Math.max(...COMMAND_SPECS.map((s) => label(s).length)) + 1;
|
|
23089
|
-
return COMMAND_SPECS.map((s) => ` ${label(s).padEnd(width)}${s.summary}`).join("\n");
|
|
23090
|
-
}
|
|
23091
|
-
|
|
23092
|
-
// src/commands/check-updates.ts
|
|
23093
|
-
import { existsSync as existsSync6 } from "fs";
|
|
23094
|
-
import { parseArgs } from "util";
|
|
23354
|
+
// ../packages/persistence/src/warn-era-cap.ts
|
|
23355
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
23356
|
+
import { join as join7 } from "path";
|
|
23095
23357
|
|
|
23096
23358
|
// ../packages/plugin-sdk/src/provider-env.ts
|
|
23097
23359
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -23112,7 +23374,7 @@ var providerEnvShape = {
|
|
|
23112
23374
|
var ProviderEnvSchema = external_exports.object(providerEnvShape);
|
|
23113
23375
|
|
|
23114
23376
|
// ../packages/plugin-sdk/src/config-inventory.ts
|
|
23115
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
23377
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync3 } from "fs";
|
|
23116
23378
|
import { homedir as homedir4 } from "os";
|
|
23117
23379
|
import { basename as basename2, join as join9 } from "path";
|
|
23118
23380
|
|
|
@@ -25043,140 +25305,597 @@ var BUNDLED_PACKS = [
|
|
|
25043
25305
|
]
|
|
25044
25306
|
},
|
|
25045
25307
|
{
|
|
25046
|
-
packId: "core-phi",
|
|
25047
|
-
name: "Protected Health Information",
|
|
25048
|
-
version: "0.1.0",
|
|
25049
|
-
rawRules: [
|
|
25050
|
-
mrn_default,
|
|
25051
|
-
hipaa_identifier_default,
|
|
25052
|
-
member_id_default,
|
|
25053
|
-
group_id_default,
|
|
25054
|
-
icd10_code_default,
|
|
25055
|
-
ndc_code_default,
|
|
25056
|
-
genetic_data_default,
|
|
25057
|
-
biometric_ref_default
|
|
25058
|
-
]
|
|
25308
|
+
packId: "core-phi",
|
|
25309
|
+
name: "Protected Health Information",
|
|
25310
|
+
version: "0.1.0",
|
|
25311
|
+
rawRules: [
|
|
25312
|
+
mrn_default,
|
|
25313
|
+
hipaa_identifier_default,
|
|
25314
|
+
member_id_default,
|
|
25315
|
+
group_id_default,
|
|
25316
|
+
icd10_code_default,
|
|
25317
|
+
ndc_code_default,
|
|
25318
|
+
genetic_data_default,
|
|
25319
|
+
biometric_ref_default
|
|
25320
|
+
]
|
|
25321
|
+
},
|
|
25322
|
+
{
|
|
25323
|
+
packId: "core-pii",
|
|
25324
|
+
name: "Core PII",
|
|
25325
|
+
version: "0.2.0",
|
|
25326
|
+
rawRules: [
|
|
25327
|
+
email_default,
|
|
25328
|
+
ssn_default,
|
|
25329
|
+
phone_us_default,
|
|
25330
|
+
phone_intl_default,
|
|
25331
|
+
ip_address_default,
|
|
25332
|
+
dob_default,
|
|
25333
|
+
drivers_license_us_default,
|
|
25334
|
+
passport_us_default,
|
|
25335
|
+
home_address_default,
|
|
25336
|
+
name_default,
|
|
25337
|
+
vin_default,
|
|
25338
|
+
mac_address_default,
|
|
25339
|
+
national_id_default,
|
|
25340
|
+
zip_default
|
|
25341
|
+
]
|
|
25342
|
+
},
|
|
25343
|
+
{
|
|
25344
|
+
packId: "secrets",
|
|
25345
|
+
name: "Secrets & Credentials",
|
|
25346
|
+
version: "0.1.0",
|
|
25347
|
+
rawRules: [
|
|
25348
|
+
aws_access_key_default,
|
|
25349
|
+
aws_secret_key_default,
|
|
25350
|
+
gcp_service_account_default,
|
|
25351
|
+
azure_connection_string_default,
|
|
25352
|
+
openai_api_key_default,
|
|
25353
|
+
anthropic_api_key_default,
|
|
25354
|
+
stripe_live_key_default,
|
|
25355
|
+
slack_token_default,
|
|
25356
|
+
discord_token_default,
|
|
25357
|
+
sendgrid_key_default,
|
|
25358
|
+
twilio_key_default,
|
|
25359
|
+
gitlab_token_default,
|
|
25360
|
+
digitalocean_token_default,
|
|
25361
|
+
datadog_key_default,
|
|
25362
|
+
npm_token_default,
|
|
25363
|
+
heroku_api_key_default,
|
|
25364
|
+
cloudflare_api_key_default,
|
|
25365
|
+
pulumi_access_token_default,
|
|
25366
|
+
terraform_cloud_token_default,
|
|
25367
|
+
vault_token_default,
|
|
25368
|
+
github_pat_default
|
|
25369
|
+
]
|
|
25370
|
+
},
|
|
25371
|
+
{
|
|
25372
|
+
packId: "secrets-infra",
|
|
25373
|
+
name: "Infrastructure Secrets",
|
|
25374
|
+
version: "0.1.0",
|
|
25375
|
+
rawRules: [
|
|
25376
|
+
ssh_private_key_default,
|
|
25377
|
+
db_connection_string_default,
|
|
25378
|
+
jwt_token_default,
|
|
25379
|
+
basic_auth_url_default,
|
|
25380
|
+
basic_auth_header_default,
|
|
25381
|
+
pgp_private_key_default,
|
|
25382
|
+
bearer_token_default,
|
|
25383
|
+
env_key_value_default,
|
|
25384
|
+
password_field_default,
|
|
25385
|
+
docker_config_auth_default,
|
|
25386
|
+
kubeconfig_token_default,
|
|
25387
|
+
api_key_header_default,
|
|
25388
|
+
generic_high_entropy_secret_default
|
|
25389
|
+
]
|
|
25390
|
+
}
|
|
25391
|
+
];
|
|
25392
|
+
|
|
25393
|
+
// ../packages/plugin-sdk/src/rule-packs.ts
|
|
25394
|
+
var BUNDLED_NAMESPACE = "aka";
|
|
25395
|
+
function registerRulePack(packId, rawRules) {
|
|
25396
|
+
const rules = rawRules.map((raw) => Rule.parse(raw));
|
|
25397
|
+
registerPack({ id: packId, rules });
|
|
25398
|
+
}
|
|
25399
|
+
function registerBundledPacks() {
|
|
25400
|
+
for (const pack of BUNDLED_PACKS) registerRulePack(pack.packId, pack.rawRules);
|
|
25401
|
+
}
|
|
25402
|
+
var cachedDetections;
|
|
25403
|
+
function bundledDetections() {
|
|
25404
|
+
cachedDetections ??= BUNDLED_PACKS.map((pack) => ({
|
|
25405
|
+
namespace: BUNDLED_NAMESPACE,
|
|
25406
|
+
packId: pack.packId,
|
|
25407
|
+
version: pack.version,
|
|
25408
|
+
name: pack.name,
|
|
25409
|
+
rules: pack.rawRules.map((raw) => Rule.parse(raw))
|
|
25410
|
+
}));
|
|
25411
|
+
return cachedDetections;
|
|
25412
|
+
}
|
|
25413
|
+
|
|
25414
|
+
// ../packages/plugin-sdk/src/repo.ts
|
|
25415
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
25416
|
+
import { basename, dirname as dirname2, isAbsolute, join as join8, sep as sep3 } from "path";
|
|
25417
|
+
function resolveRepoIdentity(cwd) {
|
|
25418
|
+
try {
|
|
25419
|
+
const root = findGitRoot(cwd);
|
|
25420
|
+
if (!root) return void 0;
|
|
25421
|
+
const ctx = resolveGitContext(root);
|
|
25422
|
+
const headRoot = ctx?.headRoot ?? root;
|
|
25423
|
+
const url2 = ctx ? remoteUrl(ctx) : void 0;
|
|
25424
|
+
return {
|
|
25425
|
+
// The path fallback is normalized to posix separators (a no-op outside
|
|
25426
|
+
// win32) so the persistence layer's `/`-separated checkout-path patterns
|
|
25427
|
+
// (the ghost sweep + the read-side worktree filter) match it as written.
|
|
25428
|
+
url: url2 ?? headRoot.split(sep3).join("/"),
|
|
25429
|
+
name: (url2 ? slugFromUrl(url2) : void 0) ?? basename(headRoot)
|
|
25430
|
+
};
|
|
25431
|
+
} catch {
|
|
25432
|
+
return void 0;
|
|
25433
|
+
}
|
|
25434
|
+
}
|
|
25435
|
+
function resolveWorktreeRoot(cwd) {
|
|
25436
|
+
try {
|
|
25437
|
+
return findGitRoot(cwd);
|
|
25438
|
+
} catch {
|
|
25439
|
+
return void 0;
|
|
25440
|
+
}
|
|
25441
|
+
}
|
|
25442
|
+
function resolveHeadRoot(cwd) {
|
|
25443
|
+
try {
|
|
25444
|
+
const root = findGitRoot(cwd);
|
|
25445
|
+
if (!root) return void 0;
|
|
25446
|
+
return resolveGitContext(root)?.headRoot ?? root;
|
|
25447
|
+
} catch {
|
|
25448
|
+
return void 0;
|
|
25449
|
+
}
|
|
25450
|
+
}
|
|
25451
|
+
function findGitRoot(start) {
|
|
25452
|
+
let dir = start;
|
|
25453
|
+
for (; ; ) {
|
|
25454
|
+
if (existsSync4(join8(dir, ".git"))) return dir;
|
|
25455
|
+
const parent = dirname2(dir);
|
|
25456
|
+
if (parent === dir) return void 0;
|
|
25457
|
+
dir = parent;
|
|
25458
|
+
}
|
|
25459
|
+
}
|
|
25460
|
+
function resolveGitContext(root) {
|
|
25461
|
+
const dotGit = join8(root, ".git");
|
|
25462
|
+
try {
|
|
25463
|
+
if (statSync2(dotGit).isDirectory()) {
|
|
25464
|
+
return { configPath: join8(dotGit, "config"), headRoot: root };
|
|
25465
|
+
}
|
|
25466
|
+
} catch {
|
|
25467
|
+
return void 0;
|
|
25468
|
+
}
|
|
25469
|
+
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
25470
|
+
if (!target) return void 0;
|
|
25471
|
+
const gitdir = isAbsolute(target) ? target : join8(root, target);
|
|
25472
|
+
if (existsSync4(join8(gitdir, "config"))) {
|
|
25473
|
+
return { configPath: join8(gitdir, "config"), headRoot: root };
|
|
25474
|
+
}
|
|
25475
|
+
const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
|
|
25476
|
+
if (!commonRaw) return void 0;
|
|
25477
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
|
|
25478
|
+
const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
|
|
25479
|
+
return { configPath: join8(commonGitDir, "config"), headRoot };
|
|
25480
|
+
}
|
|
25481
|
+
function safeRead(path) {
|
|
25482
|
+
try {
|
|
25483
|
+
return readFileSync5(path, "utf8");
|
|
25484
|
+
} catch {
|
|
25485
|
+
return void 0;
|
|
25486
|
+
}
|
|
25487
|
+
}
|
|
25488
|
+
function remoteUrl(ctx) {
|
|
25489
|
+
const config2 = safeRead(ctx.configPath);
|
|
25490
|
+
if (config2 === void 0) return void 0;
|
|
25491
|
+
const remotes = parseRemoteUrls(config2);
|
|
25492
|
+
return remotes.origin ?? Object.values(remotes)[0];
|
|
25493
|
+
}
|
|
25494
|
+
function parseRemoteUrls(config2) {
|
|
25495
|
+
const remotes = {};
|
|
25496
|
+
let current;
|
|
25497
|
+
for (const raw of config2.split("\n")) {
|
|
25498
|
+
const line = raw.trim();
|
|
25499
|
+
const header = /^\[remote "([^"]+)"\]$/.exec(line);
|
|
25500
|
+
if (header) {
|
|
25501
|
+
current = header[1];
|
|
25502
|
+
continue;
|
|
25503
|
+
}
|
|
25504
|
+
if (line.startsWith("[")) {
|
|
25505
|
+
current = void 0;
|
|
25506
|
+
continue;
|
|
25507
|
+
}
|
|
25508
|
+
if (current && !(current in remotes)) {
|
|
25509
|
+
const url2 = /^url\s*=\s*(.+)$/.exec(line)?.[1];
|
|
25510
|
+
if (url2) remotes[current] = url2.trim();
|
|
25511
|
+
}
|
|
25512
|
+
}
|
|
25513
|
+
return remotes;
|
|
25514
|
+
}
|
|
25515
|
+
function slugFromUrl(url2) {
|
|
25516
|
+
const segment = url2.replace(/\.git$/, "").split(/[/:]/).filter(Boolean).pop();
|
|
25517
|
+
return segment && segment.length > 0 ? segment : void 0;
|
|
25518
|
+
}
|
|
25519
|
+
|
|
25520
|
+
// ../packages/plugin-sdk/src/events.ts
|
|
25521
|
+
import { createHash as createHash4, randomUUID as randomUUID10 } from "crypto";
|
|
25522
|
+
|
|
25523
|
+
// ../packages/plugin-sdk/src/finding-key.ts
|
|
25524
|
+
import { createHash as createHash5 } from "crypto";
|
|
25525
|
+
|
|
25526
|
+
// ../packages/plugin-sdk/src/inventory-resolver.ts
|
|
25527
|
+
import { arch, hostname as hostname3, platform, release } from "os";
|
|
25528
|
+
|
|
25529
|
+
// ../packages/plugin-sdk/src/nudge.ts
|
|
25530
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
25531
|
+
import { join as join10 } from "path";
|
|
25532
|
+
|
|
25533
|
+
// ../packages/plugin-sdk/src/project-files.ts
|
|
25534
|
+
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
25535
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
25536
|
+
import { basename as basename3, join as join11, relative as relative2, sep as sep4 } from "path";
|
|
25537
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
25538
|
+
".git",
|
|
25539
|
+
"node_modules",
|
|
25540
|
+
".pnpm-store",
|
|
25541
|
+
".venv",
|
|
25542
|
+
"venv",
|
|
25543
|
+
"__pycache__",
|
|
25544
|
+
".next",
|
|
25545
|
+
".turbo",
|
|
25546
|
+
"dist",
|
|
25547
|
+
"build",
|
|
25548
|
+
"out",
|
|
25549
|
+
"coverage",
|
|
25550
|
+
"target",
|
|
25551
|
+
".cache"
|
|
25552
|
+
]);
|
|
25553
|
+
var MAX_FILES = 2e4;
|
|
25554
|
+
function readIgnoreLayer2(dir) {
|
|
25555
|
+
try {
|
|
25556
|
+
const content = readFileSync8(join11(dir, ".gitignore"), "utf8");
|
|
25557
|
+
return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
|
|
25558
|
+
} catch {
|
|
25559
|
+
return void 0;
|
|
25560
|
+
}
|
|
25561
|
+
}
|
|
25562
|
+
function isIgnored(layers, absPath, isDir) {
|
|
25563
|
+
let ignored = false;
|
|
25564
|
+
for (const layer of layers) {
|
|
25565
|
+
const rel = relative2(layer.base, absPath).split(sep4).join("/") + (isDir ? "/" : "");
|
|
25566
|
+
const verdict = layer.matcher.test(rel);
|
|
25567
|
+
if (verdict.ignored) ignored = true;
|
|
25568
|
+
else if (verdict.unignored) ignored = false;
|
|
25569
|
+
}
|
|
25570
|
+
return ignored;
|
|
25571
|
+
}
|
|
25572
|
+
var GENERATED_FILES = /* @__PURE__ */ new Set([
|
|
25573
|
+
"pnpm-lock.yaml",
|
|
25574
|
+
"package-lock.json",
|
|
25575
|
+
"yarn.lock",
|
|
25576
|
+
"cargo.lock",
|
|
25577
|
+
"poetry.lock",
|
|
25578
|
+
"gemfile.lock",
|
|
25579
|
+
"composer.lock",
|
|
25580
|
+
"go.sum"
|
|
25581
|
+
]);
|
|
25582
|
+
var CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([".yml", ".yaml", ".toml", ".ini", ".properties", ".conf"]);
|
|
25583
|
+
var DOCS_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".rst", ".adoc", ".txt"]);
|
|
25584
|
+
var CONFIG_TXT_BASENAMES = /* @__PURE__ */ new Set(["cmakelists.txt", "constraints.txt", "robots.txt"]);
|
|
25585
|
+
var DATA_EXTENSIONS = /* @__PURE__ */ new Set([".csv", ".tsv", ".parquet", ".sql", ".jsonl", ".ndjson"]);
|
|
25586
|
+
var CONFIG_BASENAMES = /* @__PURE__ */ new Set([
|
|
25587
|
+
"package.json",
|
|
25588
|
+
"dockerfile",
|
|
25589
|
+
"makefile",
|
|
25590
|
+
"justfile",
|
|
25591
|
+
".gitignore",
|
|
25592
|
+
".gitattributes",
|
|
25593
|
+
".npmrc",
|
|
25594
|
+
".nvmrc",
|
|
25595
|
+
".editorconfig",
|
|
25596
|
+
".akaignore"
|
|
25597
|
+
]);
|
|
25598
|
+
function classifyOrigin(relPath, name) {
|
|
25599
|
+
const lowerName = name.toLowerCase();
|
|
25600
|
+
const dot = lowerName.lastIndexOf(".");
|
|
25601
|
+
const ext = dot > 0 ? lowerName.slice(dot) : "";
|
|
25602
|
+
const segments = relPath.split("/");
|
|
25603
|
+
if (GENERATED_FILES.has(lowerName) || lowerName.includes(".generated.") || segments.includes("__generated__") || segments.includes("generated") || ext === ".map" || lowerName.endsWith(".min.js") || lowerName.endsWith(".min.css")) {
|
|
25604
|
+
return "generated";
|
|
25605
|
+
}
|
|
25606
|
+
if (segments.includes("vendor") || segments.includes("third_party")) return "vendored";
|
|
25607
|
+
if (CONFIG_TXT_BASENAMES.has(lowerName) || ext === ".txt" && lowerName.startsWith("requirements")) {
|
|
25608
|
+
return "config";
|
|
25609
|
+
}
|
|
25610
|
+
if (DOCS_EXTENSIONS.has(ext) || lowerName.startsWith("license") || segments[0] === "docs") {
|
|
25611
|
+
return "docs";
|
|
25612
|
+
}
|
|
25613
|
+
if (DATA_EXTENSIONS.has(ext) || segments.includes("fixtures") || segments.includes("__fixtures__")) {
|
|
25614
|
+
return "data";
|
|
25615
|
+
}
|
|
25616
|
+
if (CONFIG_BASENAMES.has(lowerName) || CONFIG_EXTENSIONS.has(ext) || lowerName.startsWith(".") || lowerName.includes(".config.") || lowerName.startsWith("tsconfig")) {
|
|
25617
|
+
return "config";
|
|
25618
|
+
}
|
|
25619
|
+
return "source";
|
|
25620
|
+
}
|
|
25621
|
+
function resolveProjectFiles(cwd) {
|
|
25622
|
+
try {
|
|
25623
|
+
let visit3 = function(dir, layers) {
|
|
25624
|
+
let dirents;
|
|
25625
|
+
try {
|
|
25626
|
+
dirents = readdirSync3(dir, { withFileTypes: true, encoding: "utf8" });
|
|
25627
|
+
} catch {
|
|
25628
|
+
walk.lostSubtree = true;
|
|
25629
|
+
return false;
|
|
25630
|
+
}
|
|
25631
|
+
const layer = readIgnoreLayer2(dir);
|
|
25632
|
+
const dirLayers = layer ? [...layers, layer] : layers;
|
|
25633
|
+
for (const entry of dirents) {
|
|
25634
|
+
const fullPath = join11(dir, entry.name);
|
|
25635
|
+
if (entry.isDirectory()) {
|
|
25636
|
+
if (SKIP_DIRS2.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
|
|
25637
|
+
if (existsSync5(join11(fullPath, ".git"))) continue;
|
|
25638
|
+
if (visit3(fullPath, dirLayers)) return true;
|
|
25639
|
+
continue;
|
|
25640
|
+
}
|
|
25641
|
+
if (!entry.isFile()) continue;
|
|
25642
|
+
if (entry.name === ".git") continue;
|
|
25643
|
+
if (isIgnored(dirLayers, fullPath, false)) continue;
|
|
25644
|
+
if (files.length >= MAX_FILES) return true;
|
|
25645
|
+
const relPath = relative2(root, fullPath).split(sep4).join("/");
|
|
25646
|
+
files.push({
|
|
25647
|
+
path: relPath,
|
|
25648
|
+
name: basename3(entry.name),
|
|
25649
|
+
origin: classifyOrigin(relPath, entry.name),
|
|
25650
|
+
defaultAccess: "approved"
|
|
25651
|
+
});
|
|
25652
|
+
}
|
|
25653
|
+
return false;
|
|
25654
|
+
};
|
|
25655
|
+
var visit2 = visit3;
|
|
25656
|
+
const worktree = resolveWorktreeRoot(cwd);
|
|
25657
|
+
if (!worktree) return void 0;
|
|
25658
|
+
const root = worktree;
|
|
25659
|
+
const isLinkedCheckout = resolveHeadRoot(cwd) !== worktree;
|
|
25660
|
+
const files = [];
|
|
25661
|
+
const walk = { lostSubtree: false };
|
|
25662
|
+
const truncated = visit3(root, []) || walk.lostSubtree || isLinkedCheckout;
|
|
25663
|
+
if (files.length === 0) return void 0;
|
|
25664
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
25665
|
+
return { files, truncated, scannedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
25666
|
+
} catch {
|
|
25667
|
+
return void 0;
|
|
25668
|
+
}
|
|
25669
|
+
}
|
|
25670
|
+
|
|
25671
|
+
// ../packages/plugin-sdk/src/runtime.ts
|
|
25672
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
25673
|
+
|
|
25674
|
+
// ../packages/plugin-sdk/src/suppressions.ts
|
|
25675
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25676
|
+
|
|
25677
|
+
// ../packages/plugin-sdk/src/throttle.ts
|
|
25678
|
+
import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
25679
|
+
import { join as join12 } from "path";
|
|
25680
|
+
|
|
25681
|
+
// ../packages/local-ops/src/project-inventory.ts
|
|
25682
|
+
function recordProjectInventory(db, target) {
|
|
25683
|
+
try {
|
|
25684
|
+
const abs = resolve(target);
|
|
25685
|
+
statSync5(abs);
|
|
25686
|
+
const identity = resolveRepoIdentity(abs);
|
|
25687
|
+
if (!identity) return null;
|
|
25688
|
+
const sourceProjectId2 = db.sourceProject.upsert({
|
|
25689
|
+
url: identity.url,
|
|
25690
|
+
name: identity.name,
|
|
25691
|
+
attributes: {}
|
|
25692
|
+
});
|
|
25693
|
+
const scan2 = resolveProjectFiles(abs);
|
|
25694
|
+
if (scan2) db.recordProjectFiles(sourceProjectId2, scan2);
|
|
25695
|
+
const headRoot = resolveHeadRoot(abs);
|
|
25696
|
+
const worktreeRoot = resolveWorktreeRoot(abs);
|
|
25697
|
+
if (headRoot && worktreeRoot) {
|
|
25698
|
+
db.reconcileWorktreeProjects(sourceProjectId2, headRoot, worktreeRoot);
|
|
25699
|
+
}
|
|
25700
|
+
return {
|
|
25701
|
+
projectId: sourceProjectId2,
|
|
25702
|
+
name: identity.name,
|
|
25703
|
+
url: identity.url,
|
|
25704
|
+
fileCount: scan2?.files.length ?? 0,
|
|
25705
|
+
truncated: scan2?.truncated ?? false
|
|
25706
|
+
};
|
|
25707
|
+
} catch {
|
|
25708
|
+
return null;
|
|
25709
|
+
}
|
|
25710
|
+
}
|
|
25711
|
+
|
|
25712
|
+
// ../packages/local-ops/src/update-cache.ts
|
|
25713
|
+
import { spawn } from "child_process";
|
|
25714
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
25715
|
+
import { join as join13 } from "path";
|
|
25716
|
+
var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
25717
|
+
function cachePath(home) {
|
|
25718
|
+
return join13(dataDir(home), "update-check.json");
|
|
25719
|
+
}
|
|
25720
|
+
function readCache(home) {
|
|
25721
|
+
const path = cachePath(home);
|
|
25722
|
+
if (!existsSync6(path)) return null;
|
|
25723
|
+
try {
|
|
25724
|
+
const raw = JSON.parse(readFileSync9(path, "utf8"));
|
|
25725
|
+
if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
|
|
25726
|
+
return raw;
|
|
25727
|
+
} catch {
|
|
25728
|
+
return null;
|
|
25729
|
+
}
|
|
25730
|
+
}
|
|
25731
|
+
function writeCache(home, cache) {
|
|
25732
|
+
if (!existsSync6(dataDir(home))) return;
|
|
25733
|
+
try {
|
|
25734
|
+
writeFileSync6(cachePath(home), `${JSON.stringify(cache, null, 2)}
|
|
25735
|
+
`);
|
|
25736
|
+
} catch {
|
|
25737
|
+
}
|
|
25738
|
+
}
|
|
25739
|
+
function isStale(cache, ttlMs = CHECK_TTL_MS) {
|
|
25740
|
+
return Date.now() - cache.checkedAt >= ttlMs;
|
|
25741
|
+
}
|
|
25742
|
+
function clearCache(home) {
|
|
25743
|
+
try {
|
|
25744
|
+
rmSync2(cachePath(home), { force: true });
|
|
25745
|
+
} catch {
|
|
25746
|
+
}
|
|
25747
|
+
}
|
|
25748
|
+
function refreshCache(home) {
|
|
25749
|
+
const previous = readCache(home);
|
|
25750
|
+
writeCache(home, {
|
|
25751
|
+
checkedAt: Date.now(),
|
|
25752
|
+
report: gatherReportLive(),
|
|
25753
|
+
notifiedPluginIds: previous?.notifiedPluginIds ?? []
|
|
25754
|
+
});
|
|
25755
|
+
}
|
|
25756
|
+
function triggerBackgroundRefresh(home) {
|
|
25757
|
+
const entry = process.argv[1];
|
|
25758
|
+
if (!entry) return;
|
|
25759
|
+
try {
|
|
25760
|
+
spawn(process.execPath, [entry, "__update-refresh", "--home", home], {
|
|
25761
|
+
detached: true,
|
|
25762
|
+
stdio: "ignore"
|
|
25763
|
+
}).unref();
|
|
25764
|
+
} catch {
|
|
25765
|
+
}
|
|
25766
|
+
}
|
|
25767
|
+
function notifyFromCache(home, opts) {
|
|
25768
|
+
if (!opts.isTty) return;
|
|
25769
|
+
const cache = readCache(home);
|
|
25770
|
+
if (cache) {
|
|
25771
|
+
const lines = [];
|
|
25772
|
+
for (const s of cache.report.statuses) {
|
|
25773
|
+
if (!s.updateAvailable || !s.latest) continue;
|
|
25774
|
+
const cmd = s.kind === "cli" ? "aka update" : `aka update ${s.id}`;
|
|
25775
|
+
lines.push(` \u2B06 ${s.name}: ${s.installed ?? "\u2014"} \u2192 ${s.latest} available. Run \`${cmd}\`.`);
|
|
25776
|
+
}
|
|
25777
|
+
const alreadyShown = new Set(cache.notifiedPluginIds);
|
|
25778
|
+
const freshlyShown = [];
|
|
25779
|
+
for (const p of cache.report.availablePlugins) {
|
|
25780
|
+
if (alreadyShown.has(p.id)) continue;
|
|
25781
|
+
lines.push(
|
|
25782
|
+
` \u2728 New plugin available: ${p.name}. Install with \`aka plugins install ${p.id}\`.`
|
|
25783
|
+
);
|
|
25784
|
+
freshlyShown.push(p.id);
|
|
25785
|
+
}
|
|
25786
|
+
if (lines.length > 0) {
|
|
25787
|
+
process.stderr.write(`
|
|
25788
|
+
${lines.join("\n")}
|
|
25789
|
+
`);
|
|
25790
|
+
}
|
|
25791
|
+
if (freshlyShown.length > 0) {
|
|
25792
|
+
writeCache(home, {
|
|
25793
|
+
...cache,
|
|
25794
|
+
notifiedPluginIds: [...cache.notifiedPluginIds, ...freshlyShown]
|
|
25795
|
+
});
|
|
25796
|
+
}
|
|
25797
|
+
}
|
|
25798
|
+
if (!cache || isStale(cache)) triggerBackgroundRefresh(home);
|
|
25799
|
+
}
|
|
25800
|
+
|
|
25801
|
+
// ../packages/local-ops/src/update-render.ts
|
|
25802
|
+
function statusLabel(s) {
|
|
25803
|
+
if (s.installed === null || s.latest === null) return "unknown";
|
|
25804
|
+
return s.updateAvailable ? "update available" : "up to date";
|
|
25805
|
+
}
|
|
25806
|
+
function renderReport(report) {
|
|
25807
|
+
const rows = report.statuses.map((s) => ({
|
|
25808
|
+
name: s.name,
|
|
25809
|
+
installed: s.installed ?? "\u2014",
|
|
25810
|
+
latest: s.latest ?? "unknown",
|
|
25811
|
+
status: statusLabel(s)
|
|
25812
|
+
}));
|
|
25813
|
+
const nameW = Math.max(9, ...rows.map((r) => r.name.length));
|
|
25814
|
+
const instW = Math.max(9, ...rows.map((r) => r.installed.length));
|
|
25815
|
+
const latW = Math.max(6, ...rows.map((r) => r.latest.length));
|
|
25816
|
+
const lines = [];
|
|
25817
|
+
lines.push(
|
|
25818
|
+
` ${"Component".padEnd(nameW)} ${"Installed".padEnd(instW)} ${"Latest".padEnd(latW)} Status`
|
|
25819
|
+
);
|
|
25820
|
+
for (const r of rows) {
|
|
25821
|
+
lines.push(
|
|
25822
|
+
` ${r.name.padEnd(nameW)} ${r.installed.padEnd(instW)} ${r.latest.padEnd(latW)} ${r.status}`
|
|
25823
|
+
);
|
|
25824
|
+
}
|
|
25825
|
+
if (report.availablePlugins.length > 0) {
|
|
25826
|
+
lines.push("");
|
|
25827
|
+
lines.push(" Available plugins (not installed):");
|
|
25828
|
+
for (const p of report.availablePlugins) {
|
|
25829
|
+
const version2 = p.latest ? ` v${p.latest}` : "";
|
|
25830
|
+
lines.push(` ${p.name} (${p.id})${version2} \u2014 install: aka plugins install ${p.id}`);
|
|
25831
|
+
}
|
|
25832
|
+
}
|
|
25833
|
+
return lines.join("\n");
|
|
25834
|
+
}
|
|
25835
|
+
function outdated(report) {
|
|
25836
|
+
return report.statuses.filter((s) => s.updateAvailable);
|
|
25837
|
+
}
|
|
25838
|
+
|
|
25839
|
+
// src/command-manifest.ts
|
|
25840
|
+
var COMMAND_SPECS = [
|
|
25841
|
+
{ name: "init", summary: "Scaffold ~/.aka (settings + local database)" },
|
|
25842
|
+
{
|
|
25843
|
+
name: "scan",
|
|
25844
|
+
argHint: "[path]",
|
|
25845
|
+
summary: "Scan a file or directory and record findings (default: .)",
|
|
25846
|
+
completesFiles: true
|
|
25847
|
+
},
|
|
25848
|
+
{ name: "stats", summary: "Print findings / enforcement / detections from the local store" },
|
|
25849
|
+
{
|
|
25850
|
+
name: "detections",
|
|
25851
|
+
argHint: "[update]",
|
|
25852
|
+
summary: "List installed detection packs + available updates (updates are manual)",
|
|
25853
|
+
args: ["update"]
|
|
25854
|
+
},
|
|
25855
|
+
{
|
|
25856
|
+
name: "plugins",
|
|
25857
|
+
summary: "List / install agent plugins (the CLI is an optional hub)",
|
|
25858
|
+
args: ["list", "install"]
|
|
25859
|
+
},
|
|
25860
|
+
{ name: "dashboard", summary: "Launch the local web dashboard and open it in the browser" },
|
|
25861
|
+
{
|
|
25862
|
+
name: "exception",
|
|
25863
|
+
summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
|
|
25864
|
+
args: ["approve", "add", "list", "show", "revoke", "rotate-key"]
|
|
25865
|
+
},
|
|
25866
|
+
{
|
|
25867
|
+
name: "tui",
|
|
25868
|
+
argHint: "[view]",
|
|
25869
|
+
summary: "Interactive colour dashboard \u2014 health|findings|recommend|audit (needs a TTY)",
|
|
25870
|
+
args: ["health", "findings", "recommend", "audit"]
|
|
25059
25871
|
},
|
|
25060
25872
|
{
|
|
25061
|
-
|
|
25062
|
-
|
|
25063
|
-
version: "0.2.0",
|
|
25064
|
-
rawRules: [
|
|
25065
|
-
email_default,
|
|
25066
|
-
ssn_default,
|
|
25067
|
-
phone_us_default,
|
|
25068
|
-
phone_intl_default,
|
|
25069
|
-
ip_address_default,
|
|
25070
|
-
dob_default,
|
|
25071
|
-
drivers_license_us_default,
|
|
25072
|
-
passport_us_default,
|
|
25073
|
-
home_address_default,
|
|
25074
|
-
name_default,
|
|
25075
|
-
vin_default,
|
|
25076
|
-
mac_address_default,
|
|
25077
|
-
national_id_default,
|
|
25078
|
-
zip_default
|
|
25079
|
-
]
|
|
25873
|
+
name: "check-updates",
|
|
25874
|
+
summary: "Show which components (CLI + plugins) have updates available"
|
|
25080
25875
|
},
|
|
25081
25876
|
{
|
|
25082
|
-
|
|
25083
|
-
|
|
25084
|
-
|
|
25085
|
-
|
|
25086
|
-
aws_access_key_default,
|
|
25087
|
-
aws_secret_key_default,
|
|
25088
|
-
gcp_service_account_default,
|
|
25089
|
-
azure_connection_string_default,
|
|
25090
|
-
openai_api_key_default,
|
|
25091
|
-
anthropic_api_key_default,
|
|
25092
|
-
stripe_live_key_default,
|
|
25093
|
-
slack_token_default,
|
|
25094
|
-
discord_token_default,
|
|
25095
|
-
sendgrid_key_default,
|
|
25096
|
-
twilio_key_default,
|
|
25097
|
-
gitlab_token_default,
|
|
25098
|
-
digitalocean_token_default,
|
|
25099
|
-
datadog_key_default,
|
|
25100
|
-
npm_token_default,
|
|
25101
|
-
heroku_api_key_default,
|
|
25102
|
-
cloudflare_api_key_default,
|
|
25103
|
-
pulumi_access_token_default,
|
|
25104
|
-
terraform_cloud_token_default,
|
|
25105
|
-
vault_token_default,
|
|
25106
|
-
github_pat_default
|
|
25107
|
-
]
|
|
25877
|
+
name: "update",
|
|
25878
|
+
argHint: "[what]",
|
|
25879
|
+
summary: "Update the CLI and/or plugins (what: cli | <plugin-id> | all)",
|
|
25880
|
+
args: ["cli", "all"]
|
|
25108
25881
|
},
|
|
25109
25882
|
{
|
|
25110
|
-
|
|
25111
|
-
|
|
25112
|
-
|
|
25113
|
-
|
|
25114
|
-
ssh_private_key_default,
|
|
25115
|
-
db_connection_string_default,
|
|
25116
|
-
jwt_token_default,
|
|
25117
|
-
basic_auth_url_default,
|
|
25118
|
-
basic_auth_header_default,
|
|
25119
|
-
pgp_private_key_default,
|
|
25120
|
-
bearer_token_default,
|
|
25121
|
-
env_key_value_default,
|
|
25122
|
-
password_field_default,
|
|
25123
|
-
docker_config_auth_default,
|
|
25124
|
-
kubeconfig_token_default,
|
|
25125
|
-
api_key_header_default,
|
|
25126
|
-
generic_high_entropy_secret_default
|
|
25127
|
-
]
|
|
25883
|
+
name: "completion",
|
|
25884
|
+
argHint: "<sh>",
|
|
25885
|
+
summary: "Emit a shell completion script (sh: zsh | bash)",
|
|
25886
|
+
args: ["zsh", "bash"]
|
|
25128
25887
|
}
|
|
25129
25888
|
];
|
|
25130
|
-
|
|
25131
|
-
|
|
25132
|
-
|
|
25133
|
-
|
|
25134
|
-
|
|
25135
|
-
registerPack({ id: packId, rules });
|
|
25136
|
-
}
|
|
25137
|
-
function registerBundledPacks() {
|
|
25138
|
-
for (const pack of BUNDLED_PACKS) registerRulePack(pack.packId, pack.rawRules);
|
|
25139
|
-
}
|
|
25140
|
-
var cachedDetections;
|
|
25141
|
-
function bundledDetections() {
|
|
25142
|
-
cachedDetections ??= BUNDLED_PACKS.map((pack) => ({
|
|
25143
|
-
namespace: BUNDLED_NAMESPACE,
|
|
25144
|
-
packId: pack.packId,
|
|
25145
|
-
version: pack.version,
|
|
25146
|
-
name: pack.name,
|
|
25147
|
-
rules: pack.rawRules.map((raw) => Rule.parse(raw))
|
|
25148
|
-
}));
|
|
25149
|
-
return cachedDetections;
|
|
25889
|
+
var GLOBAL_FLAGS = ["--home", "--no-update-check", "--version", "--help"];
|
|
25890
|
+
function commandsHelp() {
|
|
25891
|
+
const label = (s) => s.argHint ? `${s.name} ${s.argHint}` : s.name;
|
|
25892
|
+
const width = Math.max(...COMMAND_SPECS.map((s) => label(s).length)) + 1;
|
|
25893
|
+
return COMMAND_SPECS.map((s) => ` ${label(s).padEnd(width)}${s.summary}`).join("\n");
|
|
25150
25894
|
}
|
|
25151
25895
|
|
|
25152
|
-
//
|
|
25153
|
-
import { existsSync as
|
|
25154
|
-
import {
|
|
25155
|
-
|
|
25156
|
-
// ../packages/plugin-sdk/src/events.ts
|
|
25157
|
-
import { createHash as createHash4, randomUUID as randomUUID10 } from "crypto";
|
|
25158
|
-
|
|
25159
|
-
// ../packages/plugin-sdk/src/finding-key.ts
|
|
25160
|
-
import { createHash as createHash5 } from "crypto";
|
|
25161
|
-
|
|
25162
|
-
// ../packages/plugin-sdk/src/inventory-resolver.ts
|
|
25163
|
-
import { arch, hostname as hostname3, platform, release } from "os";
|
|
25164
|
-
|
|
25165
|
-
// ../packages/plugin-sdk/src/nudge.ts
|
|
25166
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
25167
|
-
import { join as join10 } from "path";
|
|
25168
|
-
|
|
25169
|
-
// ../packages/plugin-sdk/src/project-files.ts
|
|
25170
|
-
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
25171
|
-
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
25172
|
-
import { basename as basename3, join as join11, relative as relative2, sep as sep4 } from "path";
|
|
25173
|
-
|
|
25174
|
-
// ../packages/plugin-sdk/src/runtime.ts
|
|
25175
|
-
import { randomUUID as randomUUID11 } from "crypto";
|
|
25176
|
-
|
|
25177
|
-
// ../packages/plugin-sdk/src/throttle.ts
|
|
25178
|
-
import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
25179
|
-
import { join as join12 } from "path";
|
|
25896
|
+
// src/commands/check-updates.ts
|
|
25897
|
+
import { existsSync as existsSync7 } from "fs";
|
|
25898
|
+
import { parseArgs } from "util";
|
|
25180
25899
|
|
|
25181
25900
|
// src/lib/args.ts
|
|
25182
25901
|
var HOME_OPTION = { home: { type: "string" } };
|
|
@@ -25206,7 +25925,7 @@ function runCheckUpdates(argv) {
|
|
|
25206
25925
|
out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
|
|
25207
25926
|
`);
|
|
25208
25927
|
}
|
|
25209
|
-
if (
|
|
25928
|
+
if (existsSync7(dataDir(home))) {
|
|
25210
25929
|
const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
|
|
25211
25930
|
writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
|
|
25212
25931
|
}
|
|
@@ -25360,10 +26079,10 @@ function runCompletion(argv) {
|
|
|
25360
26079
|
|
|
25361
26080
|
// src/commands/dashboard.ts
|
|
25362
26081
|
import { spawn as spawn3 } from "child_process";
|
|
25363
|
-
import { existsSync as
|
|
26082
|
+
import { existsSync as existsSync8 } from "fs";
|
|
25364
26083
|
import { createRequire } from "module";
|
|
25365
26084
|
import { createServer } from "net";
|
|
25366
|
-
import { dirname as dirname3, join as
|
|
26085
|
+
import { dirname as dirname3, join as join14 } from "path";
|
|
25367
26086
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
25368
26087
|
import { parseArgs as parseArgs2 } from "util";
|
|
25369
26088
|
|
|
@@ -25383,14 +26102,14 @@ function openUrl(url2) {
|
|
|
25383
26102
|
var require2 = createRequire(import.meta.url);
|
|
25384
26103
|
var here = dirname3(fileURLToPath2(import.meta.url));
|
|
25385
26104
|
function isPortFree(port) {
|
|
25386
|
-
return new Promise((
|
|
26105
|
+
return new Promise((resolve2) => {
|
|
25387
26106
|
const probe = createServer();
|
|
25388
26107
|
probe.once("error", () => {
|
|
25389
|
-
|
|
26108
|
+
resolve2(false);
|
|
25390
26109
|
});
|
|
25391
26110
|
probe.once("listening", () => {
|
|
25392
26111
|
probe.close(() => {
|
|
25393
|
-
|
|
26112
|
+
resolve2(true);
|
|
25394
26113
|
});
|
|
25395
26114
|
});
|
|
25396
26115
|
probe.listen(port, "127.0.0.1");
|
|
@@ -25433,9 +26152,9 @@ async function runDashboard(argv) {
|
|
|
25433
26152
|
}
|
|
25434
26153
|
refreshUpdateMirror(dataDir());
|
|
25435
26154
|
const bundledServer = [
|
|
25436
|
-
|
|
25437
|
-
|
|
25438
|
-
].find((p) =>
|
|
26155
|
+
join14(here, "..", "web-ui", "server.js"),
|
|
26156
|
+
join14(here, "..", "web-ui", "web-ui", "server.js")
|
|
26157
|
+
].find((p) => existsSync8(p));
|
|
25439
26158
|
if (bundledServer) {
|
|
25440
26159
|
launchStandalone(bundledServer, port, shouldOpen, url2);
|
|
25441
26160
|
return;
|
|
@@ -25454,7 +26173,7 @@ async function runDashboard(argv) {
|
|
|
25454
26173
|
onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
|
|
25455
26174
|
`);
|
|
25456
26175
|
};
|
|
25457
|
-
if (
|
|
26176
|
+
if (existsSync8(join14(webUiDir, ".next", "BUILD_ID"))) {
|
|
25458
26177
|
startNext();
|
|
25459
26178
|
return;
|
|
25460
26179
|
}
|
|
@@ -25680,11 +26399,11 @@ function terminalPrompter() {
|
|
|
25680
26399
|
},
|
|
25681
26400
|
isInteractive: process.stdin.isTTY && process.stdout.isTTY,
|
|
25682
26401
|
ask(question) {
|
|
25683
|
-
return new Promise((
|
|
26402
|
+
return new Promise((resolve2) => {
|
|
25684
26403
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
25685
26404
|
rl.question(question, (answer) => {
|
|
25686
26405
|
rl.close();
|
|
25687
|
-
|
|
26406
|
+
resolve2(answer);
|
|
25688
26407
|
});
|
|
25689
26408
|
});
|
|
25690
26409
|
},
|
|
@@ -25695,7 +26414,7 @@ function terminalPrompter() {
|
|
|
25695
26414
|
// fatal signals: a security tool must never brick the terminal it was
|
|
25696
26415
|
// invoked from.
|
|
25697
26416
|
askHidden(question) {
|
|
25698
|
-
return new Promise((
|
|
26417
|
+
return new Promise((resolve2, reject) => {
|
|
25699
26418
|
const stdin = process.stdin;
|
|
25700
26419
|
process.stdout.write(question);
|
|
25701
26420
|
stdin.setRawMode(true);
|
|
@@ -25718,7 +26437,7 @@ function terminalPrompter() {
|
|
|
25718
26437
|
restore();
|
|
25719
26438
|
process.stdout.write("\n");
|
|
25720
26439
|
if (err) reject(err);
|
|
25721
|
-
else
|
|
26440
|
+
else resolve2(state.value);
|
|
25722
26441
|
};
|
|
25723
26442
|
const onError = (err) => {
|
|
25724
26443
|
finish(err);
|
|
@@ -25881,6 +26600,8 @@ function viaLabel(via) {
|
|
|
25881
26600
|
return "web add";
|
|
25882
26601
|
case "api":
|
|
25883
26602
|
return "api";
|
|
26603
|
+
case "setup-triage":
|
|
26604
|
+
return "setup triage";
|
|
25884
26605
|
}
|
|
25885
26606
|
}
|
|
25886
26607
|
function scopeSummary(ex) {
|
|
@@ -26387,50 +27108,14 @@ async function runException(argv, io = terminalPrompter()) {
|
|
|
26387
27108
|
}
|
|
26388
27109
|
|
|
26389
27110
|
// src/commands/init.ts
|
|
26390
|
-
import { existsSync as
|
|
26391
|
-
import { join as
|
|
26392
|
-
import
|
|
26393
|
-
|
|
26394
|
-
const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
|
|
26395
|
-
const home = homeBase(values.home);
|
|
26396
|
-
ensureLayoutDirSync(home);
|
|
26397
|
-
const settings = settingsDir(home);
|
|
26398
|
-
ensureLayoutDirSync(settings);
|
|
26399
|
-
const settingsFile = join14(settings, "settings.json");
|
|
26400
|
-
const settingsCreated = !existsSync8(settingsFile);
|
|
26401
|
-
if (settingsCreated) {
|
|
26402
|
-
const tmp = `${settingsFile}.tmp`;
|
|
26403
|
-
writeFileSync6(tmp, `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
|
|
26404
|
-
`, {
|
|
26405
|
-
mode: DATA_FILE_MODE
|
|
26406
|
-
});
|
|
26407
|
-
renameSync5(tmp, settingsFile);
|
|
26408
|
-
}
|
|
26409
|
-
const db = openLocalDatabase(dataDir(home));
|
|
26410
|
-
let policyCount;
|
|
26411
|
-
let packCount;
|
|
26412
|
-
let updatesAvailable;
|
|
26413
|
-
try {
|
|
26414
|
-
db.installedPacks.recordInventory(bundledDetections(), cliRecordedBy());
|
|
26415
|
-
policyCount = (await db.policies.readPolicies()).length;
|
|
26416
|
-
packCount = (await db.installedPacks.counts()).packs;
|
|
26417
|
-
updatesAvailable = (await db.detections.listDetections({ filter: "all" })).counts.updates;
|
|
26418
|
-
} finally {
|
|
26419
|
-
db.close();
|
|
26420
|
-
}
|
|
26421
|
-
process.stdout.write(
|
|
26422
|
-
`\u2713 Initialized AKA at ${home}
|
|
26423
|
-
settings: ${settingsFile}${settingsCreated ? "" : " (kept existing)"}
|
|
26424
|
-
database: ${dbPath(home)}
|
|
26425
|
-
seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
|
|
26426
|
-
` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
|
|
26427
|
-
` : "")
|
|
26428
|
-
);
|
|
26429
|
-
}
|
|
27111
|
+
import { existsSync as existsSync10, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
27112
|
+
import { join as join15 } from "path";
|
|
27113
|
+
import * as readline from "readline/promises";
|
|
27114
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
26430
27115
|
|
|
26431
27116
|
// src/commands/plugins.ts
|
|
26432
27117
|
import { existsSync as existsSync9 } from "fs";
|
|
26433
|
-
import { parseArgs as
|
|
27118
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
26434
27119
|
function runPlugins(argv) {
|
|
26435
27120
|
const [sub, ...rest] = argv;
|
|
26436
27121
|
if (!sub || sub === "list") return listPlugins(rest);
|
|
@@ -26443,7 +27128,7 @@ function runPlugins(argv) {
|
|
|
26443
27128
|
process.exitCode = 1;
|
|
26444
27129
|
}
|
|
26445
27130
|
async function listPlugins(argv) {
|
|
26446
|
-
const { values } =
|
|
27131
|
+
const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
|
|
26447
27132
|
const home = homeBase(values.home);
|
|
26448
27133
|
const active = /* @__PURE__ */ new Set();
|
|
26449
27134
|
if (existsSync9(dbPath(home))) {
|
|
@@ -26471,7 +27156,7 @@ async function listPlugins(argv) {
|
|
|
26471
27156
|
out.write("Update: aka update (or: aka check-updates)\n");
|
|
26472
27157
|
}
|
|
26473
27158
|
function installPlugin(argv) {
|
|
26474
|
-
const { positionals } =
|
|
27159
|
+
const { positionals } = parseArgs5({ args: argv, options: HOME_OPTION, allowPositionals: true });
|
|
26475
27160
|
const id = positionals[0];
|
|
26476
27161
|
if (!id) {
|
|
26477
27162
|
process.stderr.write("aka plugins install: missing <agent> (try: aka plugins list)\n");
|
|
@@ -26528,11 +27213,95 @@ Then run \`aka init\` to set up the local store.
|
|
|
26528
27213
|
}
|
|
26529
27214
|
}
|
|
26530
27215
|
|
|
27216
|
+
// src/commands/init.ts
|
|
27217
|
+
async function runInit(argv) {
|
|
27218
|
+
const { values } = parseArgs6({
|
|
27219
|
+
args: argv,
|
|
27220
|
+
options: { ...HOME_OPTION, yes: { type: "boolean", short: "y" } }
|
|
27221
|
+
});
|
|
27222
|
+
const home = homeBase(values.home);
|
|
27223
|
+
ensureLayoutDirSync(home);
|
|
27224
|
+
const settings = settingsDir(home);
|
|
27225
|
+
ensureLayoutDirSync(settings);
|
|
27226
|
+
const settingsFile = join15(settings, "settings.json");
|
|
27227
|
+
const settingsCreated = !existsSync10(settingsFile);
|
|
27228
|
+
if (settingsCreated) {
|
|
27229
|
+
const tmp = `${settingsFile}.tmp`;
|
|
27230
|
+
writeFileSync7(tmp, `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
|
|
27231
|
+
`, {
|
|
27232
|
+
mode: DATA_FILE_MODE
|
|
27233
|
+
});
|
|
27234
|
+
renameSync5(tmp, settingsFile);
|
|
27235
|
+
}
|
|
27236
|
+
const db = openLocalDatabase(dataDir(home));
|
|
27237
|
+
let policyCount;
|
|
27238
|
+
let packCount;
|
|
27239
|
+
let updatesAvailable;
|
|
27240
|
+
try {
|
|
27241
|
+
db.installedPacks.recordInventory(bundledDetections(), cliRecordedBy());
|
|
27242
|
+
policyCount = (await db.policies.readPolicies()).length;
|
|
27243
|
+
packCount = (await db.installedPacks.counts()).packs;
|
|
27244
|
+
updatesAvailable = (await db.detections.listDetections({ filter: "all" })).counts.updates;
|
|
27245
|
+
} finally {
|
|
27246
|
+
db.close();
|
|
27247
|
+
}
|
|
27248
|
+
process.stdout.write(
|
|
27249
|
+
`\u2713 Initialized AKA at ${home}
|
|
27250
|
+
settings: ${settingsFile}${settingsCreated ? "" : " (kept existing)"}
|
|
27251
|
+
database: ${dbPath(home)}
|
|
27252
|
+
seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
|
|
27253
|
+
` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
|
|
27254
|
+
` : "")
|
|
27255
|
+
);
|
|
27256
|
+
await offerPluginInstall(values.yes === true);
|
|
27257
|
+
}
|
|
27258
|
+
async function offerPluginInstall(autoYes) {
|
|
27259
|
+
const agent = findAgent("claude-code");
|
|
27260
|
+
const ref = agent ? pluginRef(agent) : void 0;
|
|
27261
|
+
if (!agent || !ref) return;
|
|
27262
|
+
if (installedPluginVersions().has(ref)) return;
|
|
27263
|
+
const out = process.stdout;
|
|
27264
|
+
if (!autoYes) {
|
|
27265
|
+
if (!process.stdin.isTTY) {
|
|
27266
|
+
out.write(
|
|
27267
|
+
`
|
|
27268
|
+
No Claude Code plugin detected. Install it via the marketplace:
|
|
27269
|
+
/plugin marketplace add ${agent.marketplaceSource ?? ""}
|
|
27270
|
+
/plugin install ${ref}
|
|
27271
|
+
Or re-run \`aka init --yes\` to install it automatically.
|
|
27272
|
+
`
|
|
27273
|
+
);
|
|
27274
|
+
return;
|
|
27275
|
+
}
|
|
27276
|
+
if (!await confirm(`
|
|
27277
|
+
Install the AKA Claude Code plugin now (via the marketplace)? [y/N] `)) {
|
|
27278
|
+
out.write("Skipped. Install anytime with `aka plugins install claude-code`.\n");
|
|
27279
|
+
return;
|
|
27280
|
+
}
|
|
27281
|
+
}
|
|
27282
|
+
await runPlugins(["install", "claude-code"]);
|
|
27283
|
+
}
|
|
27284
|
+
async function confirm(question) {
|
|
27285
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
27286
|
+
try {
|
|
27287
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
27288
|
+
return answer === "y" || answer === "yes";
|
|
27289
|
+
} finally {
|
|
27290
|
+
rl.close();
|
|
27291
|
+
}
|
|
27292
|
+
}
|
|
27293
|
+
|
|
26531
27294
|
// src/commands/scan.ts
|
|
27295
|
+
import { statSync as statSync6 } from "fs";
|
|
26532
27296
|
import { parseArgs as parseArgs7 } from "util";
|
|
26533
27297
|
function severityRank(s) {
|
|
26534
27298
|
return Severity.options.indexOf(s);
|
|
26535
27299
|
}
|
|
27300
|
+
function renderInventoryLine(inv) {
|
|
27301
|
+
if (inv.fileCount === 0) return `Inventory: ${inv.name} \xB7 file tree unchanged`;
|
|
27302
|
+
const partial2 = inv.truncated ? " (partial walk)" : "";
|
|
27303
|
+
return `Inventory: ${inv.name} \xB7 ${String(inv.fileCount)} project file(s) recorded${partial2}`;
|
|
27304
|
+
}
|
|
26536
27305
|
function runScan(argv) {
|
|
26537
27306
|
const { values, positionals } = parseArgs7({
|
|
26538
27307
|
args: argv,
|
|
@@ -26565,12 +27334,22 @@ function runScan(argv) {
|
|
|
26565
27334
|
}
|
|
26566
27335
|
failOn = parsed.data;
|
|
26567
27336
|
}
|
|
27337
|
+
try {
|
|
27338
|
+
statSync6(target);
|
|
27339
|
+
} catch {
|
|
27340
|
+
process.stderr.write(`aka scan: no such file or directory: ${target}
|
|
27341
|
+
`);
|
|
27342
|
+
process.exitCode = 1;
|
|
27343
|
+
return;
|
|
27344
|
+
}
|
|
26568
27345
|
registerBundledPacks();
|
|
26569
27346
|
const db = openLocalDatabase(dataDir(home));
|
|
26570
27347
|
let result;
|
|
27348
|
+
let inventory;
|
|
26571
27349
|
try {
|
|
26572
27350
|
const { ruleActions } = db.installedPacks.installedRuleset();
|
|
26573
27351
|
result = scanPathIntoStore(db, target, { ruleActions, sourceTool: "cli" });
|
|
27352
|
+
inventory = recordProjectInventory(db, target);
|
|
26574
27353
|
} finally {
|
|
26575
27354
|
db.close();
|
|
26576
27355
|
}
|
|
@@ -26588,8 +27367,14 @@ function runScan(argv) {
|
|
|
26588
27367
|
confidence: d.confidence
|
|
26589
27368
|
}))
|
|
26590
27369
|
);
|
|
27370
|
+
const inventoryJson = inventory ? {
|
|
27371
|
+
name: inventory.name,
|
|
27372
|
+
url: inventory.url,
|
|
27373
|
+
fileCount: inventory.fileCount,
|
|
27374
|
+
truncated: inventory.truncated
|
|
27375
|
+
} : null;
|
|
26591
27376
|
process.stdout.write(
|
|
26592
|
-
`${JSON.stringify({ target, scanned: result.scanned, findings }, null, 2)}
|
|
27377
|
+
`${JSON.stringify({ target, scanned: result.scanned, findings, inventory: inventoryJson }, null, 2)}
|
|
26593
27378
|
`
|
|
26594
27379
|
);
|
|
26595
27380
|
} else {
|
|
@@ -26597,6 +27382,8 @@ function runScan(argv) {
|
|
|
26597
27382
|
`Scanned ${String(result.scanned)} file(s) under ${target} \xB7 ${String(result.findings)} finding(s) recorded
|
|
26598
27383
|
`
|
|
26599
27384
|
);
|
|
27385
|
+
if (inventory) process.stdout.write(`${renderInventoryLine(inventory)}
|
|
27386
|
+
`);
|
|
26600
27387
|
}
|
|
26601
27388
|
if (failOn !== void 0) {
|
|
26602
27389
|
const threshold = severityRank(failOn);
|
|
@@ -27335,7 +28122,7 @@ async function runTui(argv) {
|
|
|
27335
28122
|
}
|
|
27336
28123
|
|
|
27337
28124
|
// src/commands/update.ts
|
|
27338
|
-
import * as
|
|
28125
|
+
import * as readline2 from "readline/promises";
|
|
27339
28126
|
import { parseArgs as parseArgs10 } from "util";
|
|
27340
28127
|
async function runUpdate(argv) {
|
|
27341
28128
|
const { values, positionals } = parseArgs10({
|
|
@@ -27385,7 +28172,7 @@ async function runUpdate(argv) {
|
|
|
27385
28172
|
out.write("Re-run with --yes to apply (no interactive terminal detected).\n");
|
|
27386
28173
|
return;
|
|
27387
28174
|
}
|
|
27388
|
-
if (!await
|
|
28175
|
+
if (!await confirm2("Apply these updates? [y/N] ")) {
|
|
27389
28176
|
out.write("Aborted \u2014 nothing changed.\n");
|
|
27390
28177
|
return;
|
|
27391
28178
|
}
|
|
@@ -27404,8 +28191,8 @@ async function runUpdate(argv) {
|
|
|
27404
28191
|
}
|
|
27405
28192
|
if (anyFailed) process.exitCode = 1;
|
|
27406
28193
|
}
|
|
27407
|
-
async function
|
|
27408
|
-
const rl =
|
|
28194
|
+
async function confirm2(question) {
|
|
28195
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
27409
28196
|
try {
|
|
27410
28197
|
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
27411
28198
|
return answer === "y" || answer === "yes";
|