@akasecurity/ai-tc-claude-code 0.9.10 → 0.9.11
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/.claude-plugin/plugin.json +1 -1
- package/package.json +5 -5
- package/scripts/apply-suppressions.js +622 -430
- package/scripts/backfill.js +732 -504
- package/scripts/filescan.js +726 -496
- package/scripts/firstrun.js +772 -516
- package/scripts/history-sync.js +611 -418
- package/scripts/intro.js +240 -90
- package/scripts/message-display.js +604 -412
- package/scripts/onboard.js +601 -408
- package/scripts/post-model-switch.js +258 -110
- package/scripts/post-tool-use.js +880 -493
- package/scripts/pre-model-switch.js +691 -469
- package/scripts/pre-tool-use.js +728 -498
- package/scripts/query.js +845 -526
- package/scripts/reconcile.js +716 -494
- package/scripts/remediate.js +728 -498
- package/scripts/scan-worker.js +153 -58
- package/scripts/session-start.js +783 -558
- package/scripts/start-light.js +238 -88
- package/scripts/statusline.js +697 -475
- package/scripts/stop.js +490 -121
- package/scripts/sync.js +722 -494
- package/scripts/user-prompt-submit.js +725 -495
package/scripts/firstrun.js
CHANGED
|
@@ -20420,6 +20420,94 @@ function epochMillisToIso(ms) {
|
|
|
20420
20420
|
return new Date(ms).toISOString();
|
|
20421
20421
|
}
|
|
20422
20422
|
|
|
20423
|
+
// ../../packages/schema/src/security/recommendations.ts
|
|
20424
|
+
var SEVERITY_WEIGHT = {
|
|
20425
|
+
critical: 4,
|
|
20426
|
+
high: 3,
|
|
20427
|
+
medium: 2,
|
|
20428
|
+
low: 1
|
|
20429
|
+
};
|
|
20430
|
+
var SEVERITY_WEIGHT_BY_STRING = SEVERITY_WEIGHT;
|
|
20431
|
+
function severityWeight(severity) {
|
|
20432
|
+
return SEVERITY_WEIGHT_BY_STRING[severity] ?? 0;
|
|
20433
|
+
}
|
|
20434
|
+
var ADVICE = {
|
|
20435
|
+
secret: "Rotate the exposed credentials and move them out of prompts (secrets manager / env vars).",
|
|
20436
|
+
pii: "Remove or mask personal data before it reaches the model.",
|
|
20437
|
+
financial: "Strip card and account numbers; share only non-sensitive references.",
|
|
20438
|
+
phi: "Remove protected health information \u2014 it should never reach an external model.",
|
|
20439
|
+
code_context: "Confirm this proprietary code context is safe to share.",
|
|
20440
|
+
code_flaw: "Review the flagged pattern and apply the secure alternative (parameterized queries, safe deserializers, etc.).",
|
|
20441
|
+
config: "Review the setting \u2014 a hook conflict or an egress change applies to every session that follows.",
|
|
20442
|
+
custom: "Review against your organization\u2019s custom policy."
|
|
20443
|
+
};
|
|
20444
|
+
var REC_TEMPLATE = {
|
|
20445
|
+
secret: { title: "Exposed secret detected", action: "Rotate" },
|
|
20446
|
+
pii: { title: "Personal data in a prompt", action: "Remove" },
|
|
20447
|
+
financial: { title: "Financial data detected", action: "Strip" },
|
|
20448
|
+
phi: { title: "Health information detected", action: "Remove" },
|
|
20449
|
+
code_context: { title: "Proprietary code shared", action: "Review" },
|
|
20450
|
+
code_flaw: { title: "Insecure code pattern", action: "Fix" },
|
|
20451
|
+
config: { title: "Weakened configuration", action: "Review" },
|
|
20452
|
+
custom: { title: "Custom policy match", action: "Review" }
|
|
20453
|
+
};
|
|
20454
|
+
var MAX_RECOMMENDATIONS = 10;
|
|
20455
|
+
function bucketizeRecommendations(findings) {
|
|
20456
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
20457
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
20458
|
+
for (const f of findings) {
|
|
20459
|
+
const n = f.count ?? 1;
|
|
20460
|
+
byRule.set(f.ruleId, (byRule.get(f.ruleId) ?? 0) + n);
|
|
20461
|
+
const b = buckets.get(f.category) ?? {
|
|
20462
|
+
category: f.category,
|
|
20463
|
+
count: 0,
|
|
20464
|
+
categoryCount: 0,
|
|
20465
|
+
severity: f.severity,
|
|
20466
|
+
weight: 0,
|
|
20467
|
+
ruleId: f.ruleId
|
|
20468
|
+
};
|
|
20469
|
+
b.categoryCount += n;
|
|
20470
|
+
const w = severityWeight(f.severity);
|
|
20471
|
+
if (w > b.weight) {
|
|
20472
|
+
b.weight = w;
|
|
20473
|
+
b.severity = f.severity;
|
|
20474
|
+
b.ruleId = f.ruleId;
|
|
20475
|
+
}
|
|
20476
|
+
buckets.set(f.category, b);
|
|
20477
|
+
}
|
|
20478
|
+
for (const b of buckets.values()) b.count = byRule.get(b.ruleId) ?? 0;
|
|
20479
|
+
return [...buckets.values()].sort((a, b) => b.weight - a.weight || b.categoryCount - a.categoryCount).slice(0, MAX_RECOMMENDATIONS);
|
|
20480
|
+
}
|
|
20481
|
+
function buildRecommendations(findings) {
|
|
20482
|
+
return bucketizeRecommendations(findings).map((b) => {
|
|
20483
|
+
const copy = recommendationCopy(b.category);
|
|
20484
|
+
return {
|
|
20485
|
+
severity: b.severity,
|
|
20486
|
+
title: copy.title,
|
|
20487
|
+
description: copy.advice,
|
|
20488
|
+
context: `${b.ruleId} \xB7 ${String(b.count)} finding${b.count === 1 ? "" : "s"}`,
|
|
20489
|
+
action: copy.action
|
|
20490
|
+
};
|
|
20491
|
+
});
|
|
20492
|
+
}
|
|
20493
|
+
function recommendationCopy(category) {
|
|
20494
|
+
const template = REC_TEMPLATE_BY_STRING[category] ?? {
|
|
20495
|
+
title: `${category} finding`,
|
|
20496
|
+
action: "Review"
|
|
20497
|
+
};
|
|
20498
|
+
return {
|
|
20499
|
+
...template,
|
|
20500
|
+
advice: ADVICE_BY_STRING[category] ?? "Review this finding against your policy."
|
|
20501
|
+
};
|
|
20502
|
+
}
|
|
20503
|
+
var ADVICE_BY_STRING = ADVICE;
|
|
20504
|
+
var REC_TEMPLATE_BY_STRING = REC_TEMPLATE;
|
|
20505
|
+
function healthScore(summary) {
|
|
20506
|
+
const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
|
|
20507
|
+
const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
|
|
20508
|
+
return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
|
|
20509
|
+
}
|
|
20510
|
+
|
|
20423
20511
|
// ../../packages/schema/src/token/cost-model.ts
|
|
20424
20512
|
var PROVIDER_PLATFORM = /* @__PURE__ */ new Map([
|
|
20425
20513
|
["anthropic", "anthropic"],
|
|
@@ -20739,13 +20827,11 @@ var FindingGroup = external_exports.object({
|
|
|
20739
20827
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20740
20828
|
instances: external_exports.array(FindingInstance),
|
|
20741
20829
|
// Derived from instances' statuses with open-dominates precedence (see
|
|
20742
|
-
//
|
|
20830
|
+
// foldGroupStatus). Undefined only when no instance carries a status.
|
|
20743
20831
|
status: FindingStatus.optional(),
|
|
20744
|
-
// The distinct people across the WHOLE group, not just the
|
|
20745
|
-
//
|
|
20746
|
-
//
|
|
20747
|
-
// instance carries a user, or when the store supplied whole-group folds
|
|
20748
|
-
// without one.
|
|
20832
|
+
// The distinct people across the WHOLE group, not just the instances
|
|
20833
|
+
// carried here. Undefined when no instance carries a user, or when the
|
|
20834
|
+
// store supplied whole-group folds without one.
|
|
20749
20835
|
users: external_exports.array(FindingUser).optional()
|
|
20750
20836
|
}).meta({ id: "FindingGroup" });
|
|
20751
20837
|
var FindingStats = external_exports.object({
|
|
@@ -20774,21 +20860,31 @@ var FindingFacets = external_exports.object({
|
|
|
20774
20860
|
// counted under no value.
|
|
20775
20861
|
status: external_exports.array(FindingFacetItem),
|
|
20776
20862
|
// Host tool (attributes.tool_name). Present only on the instance-level
|
|
20777
|
-
// reads, which can filter by it; the
|
|
20863
|
+
// reads, which can filter by it; the type-level read omits the dimension
|
|
20778
20864
|
// because a group spans tools.
|
|
20779
20865
|
tool: external_exports.array(FindingFacetItem).optional()
|
|
20780
20866
|
}).meta({ id: "FindingFacets" });
|
|
20781
|
-
var
|
|
20782
|
-
|
|
20867
|
+
var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
|
|
20868
|
+
id: "FindingTypeSummary"
|
|
20869
|
+
});
|
|
20870
|
+
var DEFAULT_FINDING_TYPES_LIMIT = 50;
|
|
20871
|
+
var MAX_FINDING_TYPES_LIMIT = 100;
|
|
20872
|
+
var ListFindingTypesQuery = external_exports.object({
|
|
20783
20873
|
// NOTE: severity filters by Severity (critical/high/medium/low), not by
|
|
20784
|
-
// FindingAction.
|
|
20874
|
+
// FindingAction. It narrows TYPES: a type's severity is the one its newest
|
|
20875
|
+
// firing version carries, and this list pages types.
|
|
20876
|
+
//
|
|
20877
|
+
// That is NOT a claim the findings of a type share it. A rule can hold several
|
|
20878
|
+
// definition versions at different severities, so a type kept by this filter
|
|
20879
|
+
// can hold findings that individually do not match — see totals.findings on
|
|
20880
|
+
// ListFindingTypesResponse, which counts them all.
|
|
20785
20881
|
severity: external_exports.array(Severity).optional(),
|
|
20786
20882
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20787
20883
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20788
20884
|
action: external_exports.array(FindingAction).optional(),
|
|
20789
|
-
// Matches a
|
|
20790
|
-
// individual
|
|
20791
|
-
//
|
|
20885
|
+
// Matches a type's DERIVED status (see FindingGroup.status), not its
|
|
20886
|
+
// individual findings' — so a filtered row's status always reads one of the
|
|
20887
|
+
// requested values.
|
|
20792
20888
|
status: external_exports.array(FindingStatus).optional(),
|
|
20793
20889
|
q: external_exports.string().optional(),
|
|
20794
20890
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
@@ -20798,23 +20894,37 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
20798
20894
|
// from a time-scoped page (Activity's range) can carry that scope. Absent
|
|
20799
20895
|
// means all time — this list has no default window.
|
|
20800
20896
|
from: external_exports.iso.datetime().optional(),
|
|
20801
|
-
// A
|
|
20802
|
-
//
|
|
20803
|
-
//
|
|
20804
|
-
//
|
|
20805
|
-
//
|
|
20897
|
+
// A RULE id that must appear in the page even when the cursor has already
|
|
20898
|
+
// advanced past its sort position. This is what keeps the selected type
|
|
20899
|
+
// visible in the list once it paginates: the target is appended out of sort
|
|
20900
|
+
// order rather than scanned forward for. Never affects totals, facets or the
|
|
20901
|
+
// cursor. Unlike the grouped read this replaces, it names a rule only — an
|
|
20902
|
+
// instance id is resolved by `findingInstance`, which is a primary-key seek
|
|
20903
|
+
// and so is not bounded by what any page happens to hold.
|
|
20806
20904
|
includeId: external_exports.string().optional(),
|
|
20807
|
-
|
|
20808
|
-
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
20905
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
|
|
20809
20906
|
cursor: external_exports.string().optional()
|
|
20810
20907
|
});
|
|
20811
|
-
var
|
|
20908
|
+
var ListFindingTypesResponse = external_exports.object({
|
|
20812
20909
|
totals: external_exports.object({
|
|
20910
|
+
// Findings belonging to the matching TYPES — not findings that each match
|
|
20911
|
+
// the filters. The filters here select types, so a type that survives
|
|
20912
|
+
// contributes its whole instanceCount.
|
|
20913
|
+
//
|
|
20914
|
+
// `status` is the one exception, narrowed per finding via
|
|
20915
|
+
// countInstancesByStatus. `severity`, `provider` and `action` are not, so
|
|
20916
|
+
// this can exceed what the instance read reports for the same filters: a
|
|
20917
|
+
// rule whose severity moved between versions is kept on its newest and
|
|
20918
|
+
// still counts its older findings. Narrowing the other three needs
|
|
20919
|
+
// per-dimension counts the aggregate does not carry today.
|
|
20813
20920
|
findings: external_exports.number().int().nonnegative(),
|
|
20814
|
-
|
|
20921
|
+
// Counts TYPES, which is the unit this read pages. The instance read's
|
|
20922
|
+
// own totals count findings; the two deliberately answer different
|
|
20923
|
+
// questions and are never summed.
|
|
20924
|
+
types: external_exports.number().int().nonnegative()
|
|
20815
20925
|
}),
|
|
20816
20926
|
facets: FindingFacets,
|
|
20817
|
-
items: external_exports.array(
|
|
20927
|
+
items: external_exports.array(FindingTypeSummary),
|
|
20818
20928
|
nextCursor: external_exports.string().nullable(),
|
|
20819
20929
|
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
20820
20930
|
// many times that rule fired in the session's persisted transcript. Findings
|
|
@@ -20822,7 +20932,7 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
20822
20932
|
// every firing, so the two numbers legitimately differ — this map lets a
|
|
20823
20933
|
// session-scoped view show both.
|
|
20824
20934
|
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
20825
|
-
}).meta({ id: "
|
|
20935
|
+
}).meta({ id: "ListFindingTypesResponse" });
|
|
20826
20936
|
var ApplyFindingActionRequest = external_exports.object({
|
|
20827
20937
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
20828
20938
|
// it, so it is excluded from the request contract. The mapping helper
|
|
@@ -20852,12 +20962,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
|
|
|
20852
20962
|
var MAX_FLAT_FINDINGS_LIMIT = 200;
|
|
20853
20963
|
var ListFindingInstancesQuery = external_exports.object({
|
|
20854
20964
|
severity: external_exports.array(Severity).optional(),
|
|
20855
|
-
// Rule ids, the same vocabulary the
|
|
20965
|
+
// Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
|
|
20966
|
+
// ONE of them is how the master/detail view scopes its right-hand panel.
|
|
20856
20967
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20857
20968
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20858
20969
|
action: external_exports.array(FindingAction).optional(),
|
|
20859
20970
|
// Matches each instance's OWN derived status (deriveFindingStatus), unlike
|
|
20860
|
-
// the
|
|
20971
|
+
// the types query's type-level fold.
|
|
20861
20972
|
status: external_exports.array(FindingStatus).optional(),
|
|
20862
20973
|
// Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
|
|
20863
20974
|
// where the free-text `q` can only match the rendered "via Bash" label.
|
|
@@ -20874,37 +20985,47 @@ var ListFindingInstancesQuery = external_exports.object({
|
|
|
20874
20985
|
});
|
|
20875
20986
|
var ListFindingInstancesResponse = external_exports.object({
|
|
20876
20987
|
// Instances matching the filters across the whole scope, not just this
|
|
20877
|
-
// page — cursor-independent, like the
|
|
20988
|
+
// page — cursor-independent, like the types list's totals.
|
|
20878
20989
|
totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
|
|
20879
|
-
// Counts in INSTANCES here, where the
|
|
20990
|
+
// Counts in INSTANCES here, where the types response counts types. Each
|
|
20880
20991
|
// dimension still excludes its own filter.
|
|
20881
20992
|
facets: FindingFacets,
|
|
20882
20993
|
items: external_exports.array(FindingInstanceDetail),
|
|
20883
20994
|
nextCursor: external_exports.string().nullable()
|
|
20884
20995
|
}).meta({ id: "ListFindingInstancesResponse" });
|
|
20885
|
-
var
|
|
20886
|
-
//
|
|
20887
|
-
//
|
|
20888
|
-
|
|
20889
|
-
|
|
20890
|
-
|
|
20891
|
-
|
|
20892
|
-
// Folded from the instances' derived statuses with the same
|
|
20893
|
-
// open-dominates precedence a group uses.
|
|
20894
|
-
status: FindingStatus.optional(),
|
|
20895
|
-
// Distinct rules seen at this location, capped — the row shows them as
|
|
20896
|
-
// chips, and the count is what conveys scale.
|
|
20897
|
-
ruleIds: external_exports.array(external_exports.string())
|
|
20898
|
-
}).meta({ id: "FindingLocationFile" });
|
|
20899
|
-
var FindingLocationRepo = external_exports.object({
|
|
20996
|
+
var FindingLocationSummary = external_exports.object({
|
|
20997
|
+
// Opaque, stable, minted from the pair by encodeLocationId. It exists
|
|
20998
|
+
// because a location's identity is two values and a URL param carries one:
|
|
20999
|
+
// `?loc=` names a location the way `?rule=` names a type. Only ever compared
|
|
21000
|
+
// for EQUALITY — the page's selection check, this read's `includeId`, the
|
|
21001
|
+
// client's page dedupe — never decoded, and never a sort key.
|
|
21002
|
+
id: external_exports.string(),
|
|
20900
21003
|
/** Empty when the instances carried no repo attribute. */
|
|
20901
21004
|
repo: external_exports.string(),
|
|
21005
|
+
// Empty when the instances carried no file path (a prompt, or a tool call
|
|
21006
|
+
// with no file attribution). Both halves empty is a real location — usually
|
|
21007
|
+
// the largest one in a store — and is selectable like any other.
|
|
21008
|
+
file: external_exports.string(),
|
|
20902
21009
|
instanceCount: external_exports.number().int().nonnegative(),
|
|
21010
|
+
// The WORST severity present, not the first row's. It is this list's primary
|
|
21011
|
+
// sort key, so it is also what explains why a row is where it is, and it is
|
|
21012
|
+
// how a reader decides what to open without opening everything.
|
|
20903
21013
|
maxSeverity: Severity,
|
|
20904
21014
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
21015
|
+
// Folded from the instances' derived statuses with the same open-dominates
|
|
21016
|
+
// precedence a group uses, so it answers "is anything left to do here" and
|
|
21017
|
+
// not much more: a location holding 1 open among 40 resolved reads like one
|
|
21018
|
+
// holding 40 open. That loss is accepted — the panel beside this list
|
|
21019
|
+
// carries each finding's own status, and instanceCount sits next to the
|
|
21020
|
+
// badge.
|
|
20905
21021
|
status: FindingStatus.optional(),
|
|
20906
|
-
|
|
20907
|
-
|
|
21022
|
+
// Every distinct rule seen at this location, UNCAPPED — so the length is a
|
|
21023
|
+
// tally rather than a sample and a row can say how many there are. Bounded
|
|
21024
|
+
// by the ruleset, not by the store. The view bounds what it DISPLAYS.
|
|
21025
|
+
ruleIds: external_exports.array(external_exports.string())
|
|
21026
|
+
}).meta({ id: "FindingLocationSummary" });
|
|
21027
|
+
var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
|
|
21028
|
+
var MAX_FINDING_LOCATIONS_LIMIT = 100;
|
|
20908
21029
|
var ListFindingLocationsQuery = external_exports.object({
|
|
20909
21030
|
severity: external_exports.array(Severity).optional(),
|
|
20910
21031
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
@@ -20917,18 +21038,42 @@ var ListFindingLocationsQuery = external_exports.object({
|
|
|
20917
21038
|
q: external_exports.string().optional(),
|
|
20918
21039
|
sessionId: external_exports.string().optional(),
|
|
20919
21040
|
from: external_exports.iso.datetime().optional(),
|
|
20920
|
-
|
|
21041
|
+
// A LOCATION id (see FindingLocationSummary.id) that must appear in the page
|
|
21042
|
+
// even when the cursor has already advanced past its sort position — the
|
|
21043
|
+
// counterpart of ListFindingTypesQuery.includeId, and needed far more often
|
|
21044
|
+
// here. Selecting a row pushes the URL, which re-renders the server and resets
|
|
21045
|
+
// the client's page cache to page 0; with distinct (repo, file) pairs running
|
|
21046
|
+
// into the thousands, a selection sitting off page 0 is the ordinary case
|
|
21047
|
+
// rather than a deep-link corner. Never affects totals, facets or the cursor.
|
|
21048
|
+
includeId: external_exports.string().optional(),
|
|
21049
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
|
|
21050
|
+
cursor: external_exports.string().optional()
|
|
20921
21051
|
});
|
|
20922
21052
|
var ListFindingLocationsResponse = external_exports.object({
|
|
20923
21053
|
totals: external_exports.object({
|
|
21054
|
+
// Findings matching the filters across the whole scope. Unlike the types
|
|
21055
|
+
// read's same-named field this needs no caveat: the filters here narrow
|
|
21056
|
+
// per finding, so this is the sum of every row's instanceCount.
|
|
20924
21057
|
findings: external_exports.number().int().nonnegative(),
|
|
20925
|
-
|
|
20926
|
-
|
|
21058
|
+
// Counts LOCATIONS, the unit this read pages — the number the paginator
|
|
21059
|
+
// states. The facets beside it count FINDINGS (see below); a surface
|
|
21060
|
+
// showing both says which is which.
|
|
21061
|
+
locations: external_exports.number().int().nonnegative()
|
|
20927
21062
|
}),
|
|
20928
|
-
|
|
20929
|
-
|
|
20930
|
-
|
|
20931
|
-
|
|
21063
|
+
// Counts in FINDINGS, where the types response counts types, each dimension
|
|
21064
|
+
// still excluding its own filter. Deliberately not locations: counting those
|
|
21065
|
+
// needs a set of location keys per dimension per value — memory tracking the
|
|
21066
|
+
// store times the vocabulary, in a read whose scan promises flat memory —
|
|
21067
|
+
// and the cheap per-location version is not an approximation but WRONG. A
|
|
21068
|
+
// location holding {claudecode, block} and {codex, warn} would survive
|
|
21069
|
+
// provider=claudecode AND action=warn, under which no single finding
|
|
21070
|
+
// matches, so the facet would contradict the instanceCount this whole view
|
|
21071
|
+
// rests on. Findings also keep the toolbar in the same unit as the page
|
|
21072
|
+
// tally and the panel it sits above.
|
|
21073
|
+
facets: FindingFacets,
|
|
21074
|
+
/** Sorted by max severity, then most recent, then (repo, file). */
|
|
21075
|
+
items: external_exports.array(FindingLocationSummary),
|
|
21076
|
+
nextCursor: external_exports.string().nullable()
|
|
20932
21077
|
}).meta({ id: "ListFindingLocationsResponse" });
|
|
20933
21078
|
|
|
20934
21079
|
// ../../packages/schema/src/zod/meta.ts
|
|
@@ -22097,6 +22242,14 @@ var ControlPlaneErrorBody = external_exports.object({
|
|
|
22097
22242
|
message: external_exports.string().optional()
|
|
22098
22243
|
}).optional()
|
|
22099
22244
|
});
|
|
22245
|
+
var RemoteFailureKind = external_exports.enum([
|
|
22246
|
+
"unauthorized",
|
|
22247
|
+
"forbidden",
|
|
22248
|
+
"route-absent",
|
|
22249
|
+
"invalid-request",
|
|
22250
|
+
"rejected",
|
|
22251
|
+
"unreachable"
|
|
22252
|
+
]);
|
|
22100
22253
|
var AttachDeviceRequest = external_exports.object({
|
|
22101
22254
|
// This machine's own continuity id, so re-attaching ROTATES the credential
|
|
22102
22255
|
// on one machine record instead of producing a second one. Client-minted
|
|
@@ -22790,139 +22943,62 @@ function deriveFindingStatus(row) {
|
|
|
22790
22943
|
if (row.latestResolutionStatus === "dismissed") return "dismissed";
|
|
22791
22944
|
return "open";
|
|
22792
22945
|
}
|
|
22793
|
-
function distinctUsers(instances) {
|
|
22794
|
-
const seen = /* @__PURE__ */ new Set();
|
|
22795
|
-
const users = [];
|
|
22796
|
-
for (const i of instances) {
|
|
22797
|
-
if (i.user === void 0 || seen.has(i.user.id)) continue;
|
|
22798
|
-
seen.add(i.user.id);
|
|
22799
|
-
users.push(i.user);
|
|
22800
|
-
}
|
|
22801
|
-
return users;
|
|
22802
|
-
}
|
|
22803
22946
|
function sortUsers(users) {
|
|
22804
22947
|
return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
22805
22948
|
}
|
|
22806
|
-
function
|
|
22807
|
-
const overrides = opts.overrides;
|
|
22949
|
+
function buildFindingTypes(aggregates, opts = {}) {
|
|
22808
22950
|
const packNames = opts.packNames;
|
|
22809
|
-
const
|
|
22810
|
-
const
|
|
22811
|
-
|
|
22812
|
-
const
|
|
22813
|
-
|
|
22814
|
-
else byRuleId.set(row.ruleId, [row]);
|
|
22815
|
-
}
|
|
22816
|
-
const groups = [];
|
|
22817
|
-
for (const [ruleId, ruleRows] of byRuleId) {
|
|
22818
|
-
const instances = ruleRows.map((r) => {
|
|
22819
|
-
const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
|
|
22820
|
-
return {
|
|
22821
|
-
id: r.id,
|
|
22822
|
-
provider: toApiProvider(r.sourceTool),
|
|
22823
|
-
repo: r.repo,
|
|
22824
|
-
file: r.file,
|
|
22825
|
-
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
22826
|
-
...r.eventId === void 0 ? {} : { eventId: r.eventId },
|
|
22827
|
-
...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
|
|
22828
|
-
...r.user === void 0 ? {} : { user: r.user },
|
|
22829
|
-
action: toApiAction(effectiveDbAction),
|
|
22830
|
-
detectedAt: r.occurredAt,
|
|
22831
|
-
confidence: r.confidence,
|
|
22832
|
-
status: r.status
|
|
22833
|
-
};
|
|
22834
|
-
});
|
|
22835
|
-
const agg = aggregates?.get(ruleId);
|
|
22836
|
-
const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
|
|
22837
|
-
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
22838
|
-
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
22839
|
-
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
22840
|
-
);
|
|
22841
|
-
const seenProviders = /* @__PURE__ */ new Set();
|
|
22842
|
-
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
22843
|
-
if (seenProviders.has(p)) return false;
|
|
22844
|
-
seenProviders.add(p);
|
|
22845
|
-
return true;
|
|
22846
|
-
});
|
|
22847
|
-
const actionSet = new Set(
|
|
22848
|
-
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
22849
|
-
);
|
|
22951
|
+
const types = [];
|
|
22952
|
+
for (const [ruleId, agg] of aggregates) {
|
|
22953
|
+
const users = sortUsers(agg.users ?? []);
|
|
22954
|
+
const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
|
|
22955
|
+
const actionSet = new Set(agg.actionsTaken.map(toApiAction));
|
|
22850
22956
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
22851
|
-
const
|
|
22852
|
-
const
|
|
22853
|
-
id: ruleId,
|
|
22854
|
-
name: packNames?.get(ruleId) ?? null
|
|
22855
|
-
};
|
|
22856
|
-
const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
|
|
22857
|
-
const policy = { id: `category:${apiCategory}`, name: apiCategory };
|
|
22858
|
-
const match = {
|
|
22859
|
-
maskedValue: ruleRows[0]?.maskedMatch ?? "",
|
|
22860
|
-
contextPrefix: ""
|
|
22861
|
-
// empty (pending privacy review)
|
|
22862
|
-
};
|
|
22863
|
-
const status = foldGroupStatus(
|
|
22864
|
-
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
22865
|
-
);
|
|
22866
|
-
const group = {
|
|
22957
|
+
const apiCategory = toApiCategory(agg.category ?? "custom");
|
|
22958
|
+
const type = {
|
|
22867
22959
|
id: ruleId,
|
|
22868
22960
|
category: apiCategory,
|
|
22869
22961
|
subtype: ruleId,
|
|
22870
22962
|
// human label comes with pack metadata later
|
|
22871
|
-
severity,
|
|
22872
|
-
|
|
22873
|
-
|
|
22874
|
-
|
|
22875
|
-
instanceCount: agg?.instanceCount ?? instances.length,
|
|
22963
|
+
severity: agg.severity ?? "low",
|
|
22964
|
+
detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
|
|
22965
|
+
policy: { id: `category:${apiCategory}`, name: apiCategory },
|
|
22966
|
+
instanceCount: agg.instanceCount,
|
|
22876
22967
|
providers,
|
|
22877
22968
|
aggregateAction,
|
|
22878
|
-
latestDetectedAt,
|
|
22879
|
-
|
|
22880
|
-
status,
|
|
22969
|
+
latestDetectedAt: agg.latestDetectedAt,
|
|
22970
|
+
status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
|
|
22881
22971
|
...users.length > 0 ? { users } : {}
|
|
22882
22972
|
};
|
|
22883
|
-
|
|
22884
|
-
|
|
22885
|
-
|
|
22886
|
-
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
22887
|
-
}
|
|
22973
|
+
actionsCache.set(type, [...actionSet]);
|
|
22974
|
+
if (agg.searchText !== void 0) {
|
|
22975
|
+
haystackCache.set(type, buildHaystack(type, agg.searchText));
|
|
22888
22976
|
}
|
|
22889
|
-
|
|
22977
|
+
types.push(type);
|
|
22890
22978
|
}
|
|
22891
|
-
return
|
|
22979
|
+
return types;
|
|
22892
22980
|
}
|
|
22893
22981
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
22894
|
-
function buildHaystack(
|
|
22982
|
+
function buildHaystack(t, extra) {
|
|
22895
22983
|
return [
|
|
22896
|
-
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
...g.instances.map((i) => i.repo),
|
|
22902
|
-
...g.instances.map((i) => i.file),
|
|
22903
|
-
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
22904
|
-
...g.instances.map((i) => i.id),
|
|
22905
|
-
// The people: the whole group's list when the store folded one, plus the
|
|
22906
|
-
// preview's own — the two overlap, and a haystack does not mind.
|
|
22907
|
-
...(g.users ?? []).map((u) => u.name),
|
|
22908
|
-
...g.instances.map((i) => i.user?.name ?? ""),
|
|
22984
|
+
t.subtype,
|
|
22985
|
+
t.category,
|
|
22986
|
+
t.policy.name,
|
|
22987
|
+
t.id,
|
|
22988
|
+
...(t.users ?? []).map((u) => u.name),
|
|
22909
22989
|
...extra === void 0 ? [] : [extra]
|
|
22910
22990
|
].join(" ").toLowerCase();
|
|
22911
22991
|
}
|
|
22912
|
-
function
|
|
22913
|
-
const cached2 = haystackCache.get(
|
|
22992
|
+
function typeHaystack(t) {
|
|
22993
|
+
const cached2 = haystackCache.get(t);
|
|
22914
22994
|
if (cached2 !== void 0) return cached2;
|
|
22915
|
-
const haystack = buildHaystack(
|
|
22916
|
-
haystackCache.set(
|
|
22995
|
+
const haystack = buildHaystack(t);
|
|
22996
|
+
haystackCache.set(t, haystack);
|
|
22917
22997
|
return haystack;
|
|
22918
22998
|
}
|
|
22919
22999
|
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
22920
|
-
function
|
|
22921
|
-
|
|
22922
|
-
if (cached2 !== void 0) return cached2;
|
|
22923
|
-
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
22924
|
-
actionsCache.set(g, actions);
|
|
22925
|
-
return actions;
|
|
23000
|
+
function typeActions(t) {
|
|
23001
|
+
return actionsCache.get(t) ?? [];
|
|
22926
23002
|
}
|
|
22927
23003
|
function countInstancesByStatus(statusInputs, statuses) {
|
|
22928
23004
|
const statusSet = new Set(statuses);
|
|
@@ -22933,8 +23009,8 @@ function countInstancesByStatus(statusInputs, statuses) {
|
|
|
22933
23009
|
}
|
|
22934
23010
|
return sum;
|
|
22935
23011
|
}
|
|
22936
|
-
function applyFindingFilters(
|
|
22937
|
-
let filtered =
|
|
23012
|
+
function applyFindingFilters(types, opts) {
|
|
23013
|
+
let filtered = types;
|
|
22938
23014
|
if (opts.severity && opts.severity.length > 0) {
|
|
22939
23015
|
const sevSet = new Set(opts.severity);
|
|
22940
23016
|
filtered = filtered.filter((g) => sevSet.has(g.severity));
|
|
@@ -22945,7 +23021,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22945
23021
|
}
|
|
22946
23022
|
if (opts.actions && opts.actions.length > 0) {
|
|
22947
23023
|
const actionSet = new Set(opts.actions);
|
|
22948
|
-
filtered = filtered.filter((
|
|
23024
|
+
filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
|
|
22949
23025
|
}
|
|
22950
23026
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
22951
23027
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -22957,7 +23033,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22957
23033
|
}
|
|
22958
23034
|
if (opts.q) {
|
|
22959
23035
|
const q = opts.q.toLowerCase();
|
|
22960
|
-
filtered = filtered.filter((
|
|
23036
|
+
filtered = filtered.filter((t) => typeHaystack(t).includes(q));
|
|
22961
23037
|
}
|
|
22962
23038
|
return filtered;
|
|
22963
23039
|
}
|
|
@@ -22972,11 +23048,11 @@ function compareFindingGroupOrder(a, b) {
|
|
|
22972
23048
|
if (recencyDiff !== 0) return recencyDiff;
|
|
22973
23049
|
return a.id.localeCompare(b.id);
|
|
22974
23050
|
}
|
|
22975
|
-
function
|
|
22976
|
-
return [...
|
|
23051
|
+
function sortFindingTypes(types) {
|
|
23052
|
+
return [...types].sort(compareFindingGroupOrder);
|
|
22977
23053
|
}
|
|
22978
|
-
function computeFindingFacets(
|
|
22979
|
-
const forSeverity = applyFindingFilters(
|
|
23054
|
+
function computeFindingFacets(allTypes, opts) {
|
|
23055
|
+
const forSeverity = applyFindingFilters(allTypes, {
|
|
22980
23056
|
providers: opts.providers,
|
|
22981
23057
|
actions: opts.actions,
|
|
22982
23058
|
statuses: opts.statuses,
|
|
@@ -22987,7 +23063,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22987
23063
|
for (const g of forSeverity) {
|
|
22988
23064
|
severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
|
|
22989
23065
|
}
|
|
22990
|
-
const forProvider = applyFindingFilters(
|
|
23066
|
+
const forProvider = applyFindingFilters(allTypes, {
|
|
22991
23067
|
actions: opts.actions,
|
|
22992
23068
|
statuses: opts.statuses,
|
|
22993
23069
|
q: opts.q,
|
|
@@ -22998,7 +23074,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22998
23074
|
for (const g of forProvider) {
|
|
22999
23075
|
for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
|
|
23000
23076
|
}
|
|
23001
|
-
const forAction = applyFindingFilters(
|
|
23077
|
+
const forAction = applyFindingFilters(allTypes, {
|
|
23002
23078
|
providers: opts.providers,
|
|
23003
23079
|
statuses: opts.statuses,
|
|
23004
23080
|
q: opts.q,
|
|
@@ -23007,9 +23083,9 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23007
23083
|
});
|
|
23008
23084
|
const actionMap = /* @__PURE__ */ new Map();
|
|
23009
23085
|
for (const g of forAction) {
|
|
23010
|
-
for (const a of
|
|
23086
|
+
for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
23011
23087
|
}
|
|
23012
|
-
const forSubtype = applyFindingFilters(
|
|
23088
|
+
const forSubtype = applyFindingFilters(allTypes, {
|
|
23013
23089
|
providers: opts.providers,
|
|
23014
23090
|
actions: opts.actions,
|
|
23015
23091
|
statuses: opts.statuses,
|
|
@@ -23018,7 +23094,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23018
23094
|
});
|
|
23019
23095
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
23020
23096
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
23021
|
-
const forStatus = applyFindingFilters(
|
|
23097
|
+
const forStatus = applyFindingFilters(allTypes, {
|
|
23022
23098
|
providers: opts.providers,
|
|
23023
23099
|
actions: opts.actions,
|
|
23024
23100
|
q: opts.q,
|
|
@@ -23066,10 +23142,20 @@ function matchesDimension(row, opts, dimension) {
|
|
|
23066
23142
|
return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
|
|
23067
23143
|
case "tools":
|
|
23068
23144
|
return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
|
|
23145
|
+
// An EMPTY value is a real filter here, not an absent one. The location
|
|
23146
|
+
// list buckets a finding whose event recorded no repo — or no file — under
|
|
23147
|
+
// the empty string, and selecting that bucket has to narrow the panel to
|
|
23148
|
+
// exactly it. Only `undefined` means "no filter"; a caller that wants every
|
|
23149
|
+
// row omits the key, which every call site already does.
|
|
23150
|
+
//
|
|
23151
|
+
// Reading '' as unset is what this replaced, and it failed in the one place
|
|
23152
|
+
// it mattered: the no-repo/no-file bucket is often the largest in a real
|
|
23153
|
+
// store, and its panel dropped both predicates and returned the WHOLE scope
|
|
23154
|
+
// — a row reading 3 findings beside a panel listing every finding there is.
|
|
23069
23155
|
case "repo":
|
|
23070
|
-
return opts.repo === void 0 ||
|
|
23156
|
+
return opts.repo === void 0 || row.repo === opts.repo;
|
|
23071
23157
|
case "file":
|
|
23072
|
-
return opts.file === void 0 ||
|
|
23158
|
+
return opts.file === void 0 || row.file === opts.file;
|
|
23073
23159
|
case "q":
|
|
23074
23160
|
return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
|
|
23075
23161
|
}
|
|
@@ -23183,6 +23269,23 @@ function addToLocation(acc, row) {
|
|
|
23183
23269
|
acc.statuses.push(row.status);
|
|
23184
23270
|
acc.ruleIds.add(row.ruleId);
|
|
23185
23271
|
}
|
|
23272
|
+
function compareLocationOrder(a, b) {
|
|
23273
|
+
const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
|
|
23274
|
+
const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
|
|
23275
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
23276
|
+
if (a.latestDetectedAt !== b.latestDetectedAt) {
|
|
23277
|
+
return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
|
|
23278
|
+
}
|
|
23279
|
+
if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
|
|
23280
|
+
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
23281
|
+
return 0;
|
|
23282
|
+
}
|
|
23283
|
+
function encodeLocationId(repo, file2) {
|
|
23284
|
+
return `${encodePart(repo)}/${encodePart(file2)}`;
|
|
23285
|
+
}
|
|
23286
|
+
function encodePart(value) {
|
|
23287
|
+
return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
|
|
23288
|
+
}
|
|
23186
23289
|
|
|
23187
23290
|
// ../../packages/schema/src/zod/installed-pack.ts
|
|
23188
23291
|
var InstalledPack = external_exports.object({
|
|
@@ -23686,11 +23789,11 @@ var WorkspaceSettings = external_exports.object({
|
|
|
23686
23789
|
// covers the current payload and must be re-granted.
|
|
23687
23790
|
modelJudgeConsent: ModelJudgeConsent.optional(),
|
|
23688
23791
|
// Records that the user consented to the DEFERRED send — the outbox — along
|
|
23689
|
-
// with the payload shape and the endpoint they agreed to. Since payload
|
|
23690
|
-
// that covers
|
|
23691
|
-
// carry prompt/reply text in `content
|
|
23692
|
-
// Absent until granted, and a grant for a different endpoint
|
|
23693
|
-
// payload no longer counts.
|
|
23792
|
+
// with the payload shape and the endpoint they agreed to. Since payload v3
|
|
23793
|
+
// that covers the pre-attach backlog AND undelivered captures alike, and both
|
|
23794
|
+
// carry prompt/reply/tool-output text in `content`; the key name predates
|
|
23795
|
+
// both widenings. Absent until granted, and a grant for a different endpoint
|
|
23796
|
+
// or an older payload no longer counts.
|
|
23694
23797
|
historySyncConsent: HistorySyncConsent.optional()
|
|
23695
23798
|
});
|
|
23696
23799
|
function defaultWorkspaceSettings() {
|
|
@@ -23821,6 +23924,9 @@ var ManagedSettingKey = external_exports.enum([
|
|
|
23821
23924
|
"dataSharesInPlace",
|
|
23822
23925
|
"redactFallback"
|
|
23823
23926
|
]).meta({ id: "ManagedSettingKey" });
|
|
23927
|
+
function isManagedSettingKey(value) {
|
|
23928
|
+
return ManagedSettingKey.safeParse(value).success;
|
|
23929
|
+
}
|
|
23824
23930
|
var ManagedSettingsValues = external_exports.object({
|
|
23825
23931
|
runMode: external_exports.enum(["standalone", "attached"]).optional(),
|
|
23826
23932
|
controlPlane: external_exports.object({
|
|
@@ -23845,7 +23951,27 @@ var ManagedSettings = external_exports.object({
|
|
|
23845
23951
|
// Which of those the user may not change. A key here with no matching value
|
|
23846
23952
|
// freezes whatever the user last chose; a value with no lock is a DEFAULT
|
|
23847
23953
|
// the user may still override. The two are separable on purpose.
|
|
23848
|
-
|
|
23954
|
+
//
|
|
23955
|
+
// Parsed as NAMES rather than as the enum, and split below: a name this
|
|
23956
|
+
// build does not know is dropped from the locked set and reported, never a
|
|
23957
|
+
// reason to refuse the file. The same shape reaches an older build whenever
|
|
23958
|
+
// an administrator locks a key a newer build added, and refusing it there
|
|
23959
|
+
// ran that build entirely unmanaged — every pin and lock gone — on exactly
|
|
23960
|
+
// the fleets most likely to carry a version skew. A name outside the enum
|
|
23961
|
+
// is still never HONOURED: the lockable set stays explicit above.
|
|
23962
|
+
lockedFields: external_exports.array(external_exports.string()).default([])
|
|
23963
|
+
}).transform(({ lockedFields, ...rest }) => {
|
|
23964
|
+
const known = [];
|
|
23965
|
+
const unknown2 = [];
|
|
23966
|
+
for (const name of lockedFields) {
|
|
23967
|
+
if (isManagedSettingKey(name)) known.push(name);
|
|
23968
|
+
else unknown2.push(name);
|
|
23969
|
+
}
|
|
23970
|
+
return {
|
|
23971
|
+
...rest,
|
|
23972
|
+
lockedFields: known,
|
|
23973
|
+
...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
|
|
23974
|
+
};
|
|
23849
23975
|
}).meta({ id: "ManagedSettings" });
|
|
23850
23976
|
|
|
23851
23977
|
// ../../packages/schema/src/zod/project-files.ts
|
|
@@ -23969,7 +24095,11 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
23969
24095
|
timestamp: external_exports.iso.date(),
|
|
23970
24096
|
critical: external_exports.number().int().nonnegative(),
|
|
23971
24097
|
high: external_exports.number().int().nonnegative(),
|
|
23972
|
-
medium: external_exports.number().int().nonnegative()
|
|
24098
|
+
medium: external_exports.number().int().nonnegative(),
|
|
24099
|
+
// Optional and additive, so a producer written against the earlier
|
|
24100
|
+
// three-series contract keeps validating. A consumer plotting it resolves the
|
|
24101
|
+
// absent case itself — the chart point requires a number.
|
|
24102
|
+
low: external_exports.number().int().nonnegative().optional()
|
|
23973
24103
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
23974
24104
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
23975
24105
|
range: TimeRange,
|
|
@@ -23995,6 +24125,10 @@ var ResolvedFeedItem = external_exports.object({
|
|
|
23995
24125
|
findingKey: external_exports.string(),
|
|
23996
24126
|
ruleId: external_exports.string(),
|
|
23997
24127
|
severity: Severity,
|
|
24128
|
+
// Repository slug, and the file path RELATIVE to it. The pair is what
|
|
24129
|
+
// identifies the file: a bare path matches the same name in every repo.
|
|
24130
|
+
// Optional and additive; empty when the event carried no repo.
|
|
24131
|
+
repo: external_exports.string().optional(),
|
|
23998
24132
|
path: external_exports.string(),
|
|
23999
24133
|
// ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
|
|
24000
24134
|
// findings domain). The reader `.toISOString()`s the DB epoch-ms values.
|
|
@@ -25538,7 +25672,8 @@ var SqliteActivityRepository = class {
|
|
|
25538
25672
|
SELECT 1 FROM audit_events d
|
|
25539
25673
|
WHERE d.root_session_id = audit_events.id
|
|
25540
25674
|
AND (d.content LIKE ? ESCAPE '\\'
|
|
25541
|
-
OR json_extract(d.attributes, '$.detail')
|
|
25675
|
+
OR coalesce(json_extract(d.attributes, '$.detail'),
|
|
25676
|
+
json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
|
|
25542
25677
|
);
|
|
25543
25678
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
25544
25679
|
}
|
|
@@ -26876,23 +27011,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
26876
27011
|
)`;
|
|
26877
27012
|
|
|
26878
27013
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
26879
|
-
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
26880
|
-
var DEFAULT_LOCATIONS_LIMIT = 100;
|
|
26881
|
-
var LOCATION_RULE_IDS_CAP = 20;
|
|
26882
|
-
function compareLocationOrder(a, b) {
|
|
26883
|
-
return compareFindingGroupOrder(
|
|
26884
|
-
{
|
|
26885
|
-
severity: a.maxSeverity,
|
|
26886
|
-
latestDetectedAt: a.latestDetectedAt,
|
|
26887
|
-
id: ""
|
|
26888
|
-
},
|
|
26889
|
-
{
|
|
26890
|
-
severity: b.maxSeverity,
|
|
26891
|
-
latestDetectedAt: b.latestDetectedAt,
|
|
26892
|
-
id: ""
|
|
26893
|
-
}
|
|
26894
|
-
);
|
|
26895
|
-
}
|
|
26896
27014
|
var CONCAT_SEP = ",";
|
|
26897
27015
|
var TUPLE_SEP = "|";
|
|
26898
27016
|
function splitConcat(value) {
|
|
@@ -26944,13 +27062,48 @@ function decodeGroupCursor(cursor) {
|
|
|
26944
27062
|
return null;
|
|
26945
27063
|
}
|
|
26946
27064
|
function firstAfter(sorted, cursor) {
|
|
26947
|
-
const index = sorted.findIndex((
|
|
27065
|
+
const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
|
|
26948
27066
|
return index === -1 ? sorted.length : index;
|
|
26949
27067
|
}
|
|
26950
27068
|
function findDeepLinked(sorted, page, id) {
|
|
26951
|
-
if (page.some((
|
|
26952
|
-
return sorted.find((
|
|
27069
|
+
if (page.some((t) => t.id === id)) return void 0;
|
|
27070
|
+
return sorted.find((t) => t.id === id);
|
|
27071
|
+
}
|
|
27072
|
+
function encodeLocationCursor(location) {
|
|
27073
|
+
const payload = {
|
|
27074
|
+
sev: location.maxSeverity,
|
|
27075
|
+
t: location.latestDetectedAt,
|
|
27076
|
+
r: location.repo,
|
|
27077
|
+
f: location.file
|
|
27078
|
+
};
|
|
27079
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
26953
27080
|
}
|
|
27081
|
+
function decodeLocationCursor(cursor) {
|
|
27082
|
+
const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
27083
|
+
if (parsed2 !== void 0 && typeof parsed2.sev === "string" && typeof parsed2.t === "string" && typeof parsed2.r === "string" && typeof parsed2.f === "string") {
|
|
27084
|
+
return { maxSeverity: parsed2.sev, latestDetectedAt: parsed2.t, repo: parsed2.r, file: parsed2.f };
|
|
27085
|
+
}
|
|
27086
|
+
return null;
|
|
27087
|
+
}
|
|
27088
|
+
function firstLocationAfter(sorted, cursor) {
|
|
27089
|
+
const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
|
|
27090
|
+
return index === -1 ? sorted.length : index;
|
|
27091
|
+
}
|
|
27092
|
+
function findDeepLinkedLocation(sorted, page, id) {
|
|
27093
|
+
if (page.some((l) => l.id === id)) return void 0;
|
|
27094
|
+
return sorted.find((l) => l.id === id);
|
|
27095
|
+
}
|
|
27096
|
+
var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
27097
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
27098
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
27099
|
+
e.started_at AS occurred_at,
|
|
27100
|
+
e.source_tool AS source_tool,
|
|
27101
|
+
e.repo AS repo,
|
|
27102
|
+
e.file_path AS file,
|
|
27103
|
+
e.tool_name AS tool_name,
|
|
27104
|
+
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
27105
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
27106
|
+
${latestResolutionStatusSql("f")} AS latest_status`;
|
|
26954
27107
|
var DAY_MS3 = 864e5;
|
|
26955
27108
|
var SqliteFindingsRepository = class {
|
|
26956
27109
|
constructor(db) {
|
|
@@ -27071,30 +27224,26 @@ var SqliteFindingsRepository = class {
|
|
|
27071
27224
|
);
|
|
27072
27225
|
}
|
|
27073
27226
|
/**
|
|
27074
|
-
*
|
|
27075
|
-
*
|
|
27076
|
-
*
|
|
27077
|
-
*
|
|
27078
|
-
*
|
|
27079
|
-
*
|
|
27080
|
-
*
|
|
27081
|
-
*
|
|
27082
|
-
*
|
|
27083
|
-
*
|
|
27084
|
-
*
|
|
27085
|
-
*
|
|
27227
|
+
* Finding TYPES for the dashboard — one row per rule, scoped to the four
|
|
27228
|
+
* capture kinds (audit_events also holds structural/reconciler/scan rows this
|
|
27229
|
+
* list must never surface), with per-filter-excluded facets, the requested
|
|
27230
|
+
* filters applied, and sorted by severity then recency. Filtering and faceting
|
|
27231
|
+
* run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
|
|
27232
|
+
* full filtered set; `items` is the requested page (default 50), keyset-paged.
|
|
27233
|
+
* Under a `status` filter, `totals.findings` counts only findings whose
|
|
27234
|
+
* derived status was requested.
|
|
27235
|
+
*
|
|
27236
|
+
* ONE read, which materializes no findings: a single aggregate per rule_id,
|
|
27237
|
+
* folding EVERY finding into the numbers a type row and the filters need
|
|
27238
|
+
* (count, severity, category, providers, actions, statuses, latest, search
|
|
27239
|
+
* text). The findings OF a type come from listFindingInstances scoped to
|
|
27240
|
+
* `subtype`, so neither list bounds the other and no per-type cap exists.
|
|
27086
27241
|
*
|
|
27087
|
-
* Two reads, neither of which materializes a row per finding:
|
|
27088
|
-
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
27089
|
-
* the group and the filters need (count, providers, actions, statuses,
|
|
27090
|
-
* latest, search text);
|
|
27091
|
-
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
27092
|
-
* populate `instances` for the table's expanded rows.
|
|
27093
27242
|
* The aggregates carry raw DB values and are translated by the same
|
|
27094
|
-
* @akasecurity/schema mappers
|
|
27095
|
-
* rule is ever restated in SQL.
|
|
27243
|
+
* @akasecurity/schema mappers every other path uses, so no enum mapping or
|
|
27244
|
+
* status rule is ever restated in SQL.
|
|
27096
27245
|
*/
|
|
27097
|
-
|
|
27246
|
+
listFindingTypes(query) {
|
|
27098
27247
|
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
27099
27248
|
const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
|
|
27100
27249
|
const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
|
|
@@ -27107,12 +27256,7 @@ var SqliteFindingsRepository = class {
|
|
|
27107
27256
|
predicate,
|
|
27108
27257
|
params: sessionParams
|
|
27109
27258
|
});
|
|
27110
|
-
const
|
|
27111
|
-
sessionId: query.sessionId,
|
|
27112
|
-
from: query.from
|
|
27113
|
-
});
|
|
27114
|
-
const groupable = rows.map(toFlatFindingRow);
|
|
27115
|
-
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
27259
|
+
const allTypes = buildFindingTypes(aggregates);
|
|
27116
27260
|
const filterOpts = {
|
|
27117
27261
|
severity: query.severity,
|
|
27118
27262
|
providers: query.provider,
|
|
@@ -27121,30 +27265,25 @@ var SqliteFindingsRepository = class {
|
|
|
27121
27265
|
subtype: query.subtype,
|
|
27122
27266
|
q: query.q
|
|
27123
27267
|
};
|
|
27124
|
-
const facets = computeFindingFacets(
|
|
27125
|
-
const sorted =
|
|
27268
|
+
const facets = computeFindingFacets(allTypes, filterOpts);
|
|
27269
|
+
const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
|
|
27126
27270
|
const statusFilter = query.status ?? [];
|
|
27127
27271
|
const totals = {
|
|
27128
|
-
findings: sorted.reduce((acc,
|
|
27129
|
-
if (statusFilter.length === 0) return acc +
|
|
27130
|
-
const agg = aggregates.get(
|
|
27131
|
-
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ??
|
|
27272
|
+
findings: sorted.reduce((acc, t) => {
|
|
27273
|
+
if (statusFilter.length === 0) return acc + t.instanceCount;
|
|
27274
|
+
const agg = aggregates.get(t.id);
|
|
27275
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
|
|
27132
27276
|
}, 0),
|
|
27133
|
-
|
|
27277
|
+
types: sorted.length
|
|
27134
27278
|
};
|
|
27135
|
-
const limit = query.limit ??
|
|
27279
|
+
const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
|
|
27136
27280
|
const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
|
|
27137
27281
|
const start = cursor === null ? 0 : firstAfter(sorted, cursor);
|
|
27138
27282
|
const page = sorted.slice(start, start + limit);
|
|
27139
27283
|
const lastOnPage = page.at(-1);
|
|
27140
27284
|
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
|
|
27141
27285
|
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
|
|
27142
|
-
const
|
|
27143
|
-
const narrow = (g) => statusSet ? {
|
|
27144
|
-
...g,
|
|
27145
|
-
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
27146
|
-
} : g;
|
|
27147
|
-
const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
|
|
27286
|
+
const items = [...page, ...deepLinked ? [deepLinked] : []];
|
|
27148
27287
|
return Promise.resolve({
|
|
27149
27288
|
totals,
|
|
27150
27289
|
facets,
|
|
@@ -27155,7 +27294,7 @@ var SqliteFindingsRepository = class {
|
|
|
27155
27294
|
}
|
|
27156
27295
|
/**
|
|
27157
27296
|
* One row per rule_id, folding EVERY instance of the group into the values
|
|
27158
|
-
*
|
|
27297
|
+
* buildFindingTypes cannot recover from an aggregate. Bounded by the number of
|
|
27159
27298
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
27160
27299
|
*
|
|
27161
27300
|
* A single scan, folded in two levels: the inner SELECT groups by
|
|
@@ -27249,13 +27388,25 @@ var SqliteFindingsRepository = class {
|
|
|
27249
27388
|
});
|
|
27250
27389
|
}
|
|
27251
27390
|
/**
|
|
27252
|
-
* The same findings folded by
|
|
27391
|
+
* The same findings folded by WHERE they live — one row per (repo, file) pair.
|
|
27253
27392
|
*
|
|
27254
27393
|
* The grouping keys come from the capturing event's attributes, which is what
|
|
27255
|
-
* the local store relates a finding to
|
|
27256
|
-
*
|
|
27257
|
-
* empty-string bucket, which
|
|
27258
|
-
*
|
|
27394
|
+
* the local store relates a finding to; there is no finding↔asset row to group
|
|
27395
|
+
* by instead. A repo or file the event did not record folds into the
|
|
27396
|
+
* empty-string bucket, which is a real location like any other: it is listed,
|
|
27397
|
+
* it is selectable, and its `?loc=` token is as good as any other row's.
|
|
27398
|
+
*
|
|
27399
|
+
* ONE flat list rather than repos nesting files. A rollup can only be paged by
|
|
27400
|
+
* repo, which leaves the file list inside it unbounded — the shape the by-type
|
|
27401
|
+
* list was rebuilt to remove — and two-level pagination inside an
|
|
27402
|
+
* expand/collapse table is what pushed that view to master/detail in the first
|
|
27403
|
+
* place.
|
|
27404
|
+
*
|
|
27405
|
+
* Every filter narrows the FINDINGS and the locations fall out of what
|
|
27406
|
+
* survives, so each row's `instanceCount` is exactly what listFindingInstances
|
|
27407
|
+
* reports for the same filters scoped to that pair. The view depends on it:
|
|
27408
|
+
* one toolbar sits over both panels precisely because a location owns none of
|
|
27409
|
+
* its fields.
|
|
27259
27410
|
*/
|
|
27260
27411
|
listFindingLocations(query) {
|
|
27261
27412
|
const opts = {
|
|
@@ -27267,13 +27418,16 @@ var SqliteFindingsRepository = class {
|
|
|
27267
27418
|
tools: query.tool,
|
|
27268
27419
|
q: query.q
|
|
27269
27420
|
};
|
|
27270
|
-
const limit = query.limit ??
|
|
27421
|
+
const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
|
|
27422
|
+
const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
|
|
27271
27423
|
const byRepo = /* @__PURE__ */ new Map();
|
|
27424
|
+
const accumulator = createInstanceFacetAccumulator(opts);
|
|
27272
27425
|
let total = 0;
|
|
27273
27426
|
for (const row of this.scanFindingRows({
|
|
27274
27427
|
sessionId: query.sessionId,
|
|
27275
27428
|
from: query.from
|
|
27276
27429
|
})) {
|
|
27430
|
+
accumulator.add(row);
|
|
27277
27431
|
if (!matchesInstanceFilters(row, opts)) continue;
|
|
27278
27432
|
total += 1;
|
|
27279
27433
|
let files = byRepo.get(row.repo);
|
|
@@ -27288,103 +27442,35 @@ var SqliteFindingsRepository = class {
|
|
|
27288
27442
|
}
|
|
27289
27443
|
addToLocation(acc, row);
|
|
27290
27444
|
}
|
|
27291
|
-
|
|
27292
|
-
const
|
|
27293
|
-
|
|
27294
|
-
|
|
27295
|
-
|
|
27296
|
-
|
|
27297
|
-
|
|
27298
|
-
|
|
27299
|
-
|
|
27300
|
-
|
|
27301
|
-
|
|
27302
|
-
|
|
27303
|
-
|
|
27304
|
-
|
|
27305
|
-
|
|
27306
|
-
|
|
27307
|
-
|
|
27308
|
-
|
|
27309
|
-
|
|
27310
|
-
|
|
27311
|
-
|
|
27312
|
-
|
|
27313
|
-
);
|
|
27314
|
-
const statuses = fileRows.map((f) => f.status);
|
|
27315
|
-
const folded = foldGroupStatus(statuses);
|
|
27316
|
-
return {
|
|
27317
|
-
repo,
|
|
27318
|
-
instanceCount: rollup.instanceCount,
|
|
27319
|
-
maxSeverity: rollup.maxSeverity,
|
|
27320
|
-
latestDetectedAt: rollup.latestDetectedAt,
|
|
27321
|
-
...folded === void 0 ? {} : { status: folded },
|
|
27322
|
-
files: fileRows
|
|
27323
|
-
};
|
|
27324
|
-
});
|
|
27325
|
-
repos.sort(compareLocationOrder);
|
|
27445
|
+
const sorted = [];
|
|
27446
|
+
for (const [repo, files] of byRepo) {
|
|
27447
|
+
for (const [file2, acc] of files) {
|
|
27448
|
+
const status = foldGroupStatus(acc.statuses);
|
|
27449
|
+
sorted.push({
|
|
27450
|
+
id: encodeLocationId(repo, file2),
|
|
27451
|
+
repo,
|
|
27452
|
+
file: file2,
|
|
27453
|
+
instanceCount: acc.instanceCount,
|
|
27454
|
+
maxSeverity: acc.maxSeverity,
|
|
27455
|
+
latestDetectedAt: acc.latestDetectedAt,
|
|
27456
|
+
...status === void 0 ? {} : { status },
|
|
27457
|
+
ruleIds: [...acc.ruleIds]
|
|
27458
|
+
});
|
|
27459
|
+
}
|
|
27460
|
+
}
|
|
27461
|
+
sorted.sort(compareLocationOrder);
|
|
27462
|
+
const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
|
|
27463
|
+
const page = sorted.slice(start, start + limit);
|
|
27464
|
+
const lastOnPage = page.at(-1);
|
|
27465
|
+
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
|
|
27466
|
+
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
|
|
27326
27467
|
return Promise.resolve({
|
|
27327
|
-
totals: { findings: total,
|
|
27328
|
-
|
|
27329
|
-
|
|
27468
|
+
totals: { findings: total, locations: sorted.length },
|
|
27469
|
+
facets: accumulator.facets(),
|
|
27470
|
+
items: [...page, ...deepLinked ? [deepLinked] : []],
|
|
27471
|
+
nextCursor
|
|
27330
27472
|
});
|
|
27331
27473
|
}
|
|
27332
|
-
/**
|
|
27333
|
-
* Each group's newest instances, for the table's expanded rows.
|
|
27334
|
-
*
|
|
27335
|
-
* ONE index-ordered scan with early termination, and the shape is the point.
|
|
27336
|
-
* The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
|
|
27337
|
-
* started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
|
|
27338
|
-
* through a temp B-tree to keep a bounded preview of each group, and then
|
|
27339
|
-
* sorts the survivors again for the page order. Both sorts grow with the
|
|
27340
|
-
* store while the answer does not.
|
|
27341
|
-
*
|
|
27342
|
-
* Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
|
|
27343
|
-
* (or the session or window index the scope names — see `findingScanSql`),
|
|
27344
|
-
* which is already the order the page wants, and keeps rows per rule until
|
|
27345
|
-
* each rule has as many as it can show. The aggregate the caller already holds
|
|
27346
|
-
* says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
|
|
27347
|
-
* per rule, summed, is the number of rows this scan has to find, and it stops
|
|
27348
|
-
* on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
|
|
27349
|
-
* (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
|
|
27350
|
-
* store with many firing rules widens it. The bound that DOES hold
|
|
27351
|
-
* unconditionally is the sorted form's floor: this scan visits at most as
|
|
27352
|
-
* many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
|
|
27353
|
-
* sorted, and stops the moment every rule has its cap, where the sorted form
|
|
27354
|
-
* sorts the whole scope regardless. The true worst case — the rarest rule's
|
|
27355
|
-
* wanted instances sitting at the tail of the scope — is one pass over
|
|
27356
|
-
* everything in scope with a block sort of the id tie-break only, never a
|
|
27357
|
-
* sort of the scope, which is still that floor.
|
|
27358
|
-
*
|
|
27359
|
-
* A row whose rule the aggregate did not see is skipped: the two statements
|
|
27360
|
-
* run without a shared snapshot, so a capture landing between them can add a
|
|
27361
|
-
* rule here that has no counts there, and the counts are what the group is
|
|
27362
|
-
* built from.
|
|
27363
|
-
*/
|
|
27364
|
-
previewRows(aggregates, scope) {
|
|
27365
|
-
const wanted = /* @__PURE__ */ new Map();
|
|
27366
|
-
let remaining = 0;
|
|
27367
|
-
for (const [ruleId, agg] of aggregates) {
|
|
27368
|
-
const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
|
|
27369
|
-
wanted.set(ruleId, n);
|
|
27370
|
-
remaining += n;
|
|
27371
|
-
}
|
|
27372
|
-
const rows = [];
|
|
27373
|
-
if (remaining === 0) return rows;
|
|
27374
|
-
const { sql, params } = this.findingScanSql(scope);
|
|
27375
|
-
const taken = /* @__PURE__ */ new Map();
|
|
27376
|
-
for (const r of iterateRows(this.db.prepare(sql), params)) {
|
|
27377
|
-
const want = wanted.get(r.rule_id);
|
|
27378
|
-
if (want === void 0) continue;
|
|
27379
|
-
const have = taken.get(r.rule_id) ?? 0;
|
|
27380
|
-
if (have >= want) continue;
|
|
27381
|
-
taken.set(r.rule_id, have + 1);
|
|
27382
|
-
rows.push(r);
|
|
27383
|
-
remaining -= 1;
|
|
27384
|
-
if (remaining === 0) break;
|
|
27385
|
-
}
|
|
27386
|
-
return rows;
|
|
27387
|
-
}
|
|
27388
27474
|
/**
|
|
27389
27475
|
* Every finding in scope as a FlatFindingRow, newest first, streamed.
|
|
27390
27476
|
*
|
|
@@ -27411,6 +27497,33 @@ var SqliteFindingsRepository = class {
|
|
|
27411
27497
|
yield toFlatFindingRow(r);
|
|
27412
27498
|
}
|
|
27413
27499
|
}
|
|
27500
|
+
/**
|
|
27501
|
+
* One finding by its own id, or null when no such row exists.
|
|
27502
|
+
*
|
|
27503
|
+
* A primary-key seek on `inspection_findings`, so its cost does not grow with
|
|
27504
|
+
* the store — and, unlike anything derived from a list page, it resolves a
|
|
27505
|
+
* finding of ANY age. That is what the Findings page's one-shot `?finding=`
|
|
27506
|
+
* deep link needs: the id it carries may name a finding thousands of rows
|
|
27507
|
+
* older than anything a first page holds.
|
|
27508
|
+
*
|
|
27509
|
+
* Deliberately UNFILTERED — no capture-kind, session or time predicate. It
|
|
27510
|
+
* RESOLVES an id; whether that row would survive the list's current filters is
|
|
27511
|
+
* a different question, and hiding the target because a filter excludes it is
|
|
27512
|
+
* worse than showing it.
|
|
27513
|
+
*
|
|
27514
|
+
* `groupId` on the result IS the rule id, so this one read answers both "which
|
|
27515
|
+
* type should the list select?" and "what does the drawer show?".
|
|
27516
|
+
*/
|
|
27517
|
+
findingInstance(id) {
|
|
27518
|
+
const row = this.db.prepare(
|
|
27519
|
+
`SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27520
|
+
FROM inspection_findings f
|
|
27521
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
27522
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27523
|
+
WHERE f.id = ?`
|
|
27524
|
+
).get(id);
|
|
27525
|
+
return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
|
|
27526
|
+
}
|
|
27414
27527
|
/**
|
|
27415
27528
|
* The one statement both instance-level scans run: every finding in scope,
|
|
27416
27529
|
* joined to its event and definition, newest first.
|
|
@@ -27444,17 +27557,7 @@ var SqliteFindingsRepository = class {
|
|
|
27444
27557
|
conditions.push("e.started_at >= ?");
|
|
27445
27558
|
params.push(isoToEpochMillis(scope.from));
|
|
27446
27559
|
}
|
|
27447
|
-
const sql = `SELECT
|
|
27448
|
-
d.severity AS severity, f.masked_match AS masked_match,
|
|
27449
|
-
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
27450
|
-
e.started_at AS occurred_at,
|
|
27451
|
-
e.source_tool AS source_tool,
|
|
27452
|
-
e.repo AS repo,
|
|
27453
|
-
e.file_path AS file,
|
|
27454
|
-
e.tool_name AS tool_name,
|
|
27455
|
-
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
27456
|
-
e.event_type AS kind, f.finding_key AS finding_key,
|
|
27457
|
-
${latestResolutionStatusSql("f")} AS latest_status
|
|
27560
|
+
const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27458
27561
|
FROM audit_events e
|
|
27459
27562
|
CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
|
|
27460
27563
|
CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -27468,6 +27571,26 @@ var SqliteFindingsRepository = class {
|
|
|
27468
27571
|
group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
27469
27572
|
const rows = this.db.prepare(
|
|
27470
27573
|
`SELECT rule_id,
|
|
27574
|
+
-- BARE columns beside max(latest_at), which is deliberate and
|
|
27575
|
+
-- is SQLite's documented behaviour: with a single min()/max()
|
|
27576
|
+
-- in an aggregate query, every bare column takes its value from
|
|
27577
|
+
-- the row that produced the extremum. So these are the severity
|
|
27578
|
+
-- and category of the definition whose finding is NEWEST, which
|
|
27579
|
+
-- is what the row-based build they replaced read off its first
|
|
27580
|
+
-- (newest-first) row.
|
|
27581
|
+
--
|
|
27582
|
+
-- min() is WRONG here and was the defect: inspection_definitions
|
|
27583
|
+
-- holds one row per rule VERSION (see its writer \u2014 a version bump
|
|
27584
|
+
-- mints a new row), so a rule whose severity moved between
|
|
27585
|
+
-- versions has several, and min() picks the ALPHABETICALLY
|
|
27586
|
+
-- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
|
|
27587
|
+
-- That is arbitrary in direction, and it feeds the badge, the
|
|
27588
|
+
-- filter, the facet counts and the primary sort key.
|
|
27589
|
+
--
|
|
27590
|
+
-- Adding a second min()/max() aggregate here would make these
|
|
27591
|
+
-- bare columns ambiguous again; keep max(latest_at) the only one.
|
|
27592
|
+
severity,
|
|
27593
|
+
category,
|
|
27471
27594
|
sum(tuple_count) AS instance_count,
|
|
27472
27595
|
max(latest_at) AS latest_at,
|
|
27473
27596
|
group_concat(source_tools) AS source_tools,
|
|
@@ -27478,6 +27601,14 @@ var SqliteFindingsRepository = class {
|
|
|
27478
27601
|
group_concat(tool_names) AS tool_names
|
|
27479
27602
|
FROM (
|
|
27480
27603
|
SELECT d.rule_id AS rule_id,
|
|
27604
|
+
-- Severity and category are columns of the DEFINITION, and
|
|
27605
|
+
-- a rule can have SEVERAL definitions (one per version), so
|
|
27606
|
+
-- these are grouped on below and resolved to the newest
|
|
27607
|
+
-- firing version by the outer query's bare-column select.
|
|
27608
|
+
-- They ride the aggregate because the type build has no rows
|
|
27609
|
+
-- to read them off \u2014 see buildFindingTypes.
|
|
27610
|
+
d.severity AS severity,
|
|
27611
|
+
d.category AS category,
|
|
27481
27612
|
e.event_type || '${TUPLE_SEP}' ||
|
|
27482
27613
|
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
27483
27614
|
coalesce(latest.status, '') AS status_tuple,
|
|
@@ -27492,7 +27623,7 @@ var SqliteFindingsRepository = class {
|
|
|
27492
27623
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
27493
27624
|
ON latest.finding_key = f.finding_key
|
|
27494
27625
|
${scope.predicate}
|
|
27495
|
-
GROUP BY d.rule_id, status_tuple
|
|
27626
|
+
GROUP BY d.rule_id, d.severity, d.category, status_tuple
|
|
27496
27627
|
)
|
|
27497
27628
|
GROUP BY rule_id`
|
|
27498
27629
|
).all(scope.params);
|
|
@@ -27501,6 +27632,8 @@ var SqliteFindingsRepository = class {
|
|
|
27501
27632
|
r.rule_id,
|
|
27502
27633
|
{
|
|
27503
27634
|
instanceCount: r.instance_count,
|
|
27635
|
+
severity: r.severity,
|
|
27636
|
+
category: r.category,
|
|
27504
27637
|
sourceTools: splitConcat(r.source_tools),
|
|
27505
27638
|
actionsTaken: splitConcat(r.actions_taken),
|
|
27506
27639
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
@@ -27517,7 +27650,7 @@ var SqliteFindingsRepository = class {
|
|
|
27517
27650
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
27518
27651
|
// Free text only — joined and substring-matched, so group_concat's
|
|
27519
27652
|
// commas need no unpicking (a repo/path containing one still matches).
|
|
27520
|
-
// Left undefined (not '') when unfetched, so
|
|
27653
|
+
// Left undefined (not '') when unfetched, so buildFindingTypes can
|
|
27521
27654
|
// tell "no q this request" from "a group with no repo/file at all"
|
|
27522
27655
|
// and skip priming a haystack nothing will read.
|
|
27523
27656
|
...withSearchText ? {
|
|
@@ -27669,6 +27802,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27669
27802
|
this.markOwedStmt = db.prepare(
|
|
27670
27803
|
`UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
|
|
27671
27804
|
);
|
|
27805
|
+
this.markCaptureBacklogOwedStmt = db.prepare(
|
|
27806
|
+
`UPDATE audit_events SET outbox_owed = 1
|
|
27807
|
+
WHERE synced_at IS NULL
|
|
27808
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27809
|
+
AND started_at < :before`
|
|
27810
|
+
);
|
|
27672
27811
|
this.stampStmt = db.prepare(
|
|
27673
27812
|
`UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
|
|
27674
27813
|
);
|
|
@@ -27717,7 +27856,9 @@ var SqliteHistorySyncRepository = class {
|
|
|
27717
27856
|
);
|
|
27718
27857
|
this.disownCapturesStmt = db.prepare(
|
|
27719
27858
|
`UPDATE audit_events SET outbox_owed = NULL
|
|
27720
|
-
WHERE outbox_owed IS NOT NULL
|
|
27859
|
+
WHERE outbox_owed IS NOT NULL
|
|
27860
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27861
|
+
AND started_at < :attachedAt`
|
|
27721
27862
|
);
|
|
27722
27863
|
this.rearmStmt = db.prepare(
|
|
27723
27864
|
`UPDATE audit_events SET synced_at = NULL
|
|
@@ -27793,6 +27934,7 @@ var SqliteHistorySyncRepository = class {
|
|
|
27793
27934
|
freezeBoundaryStmt;
|
|
27794
27935
|
captureRowsStmt;
|
|
27795
27936
|
markOwedStmt;
|
|
27937
|
+
markCaptureBacklogOwedStmt;
|
|
27796
27938
|
captureSkipCountStmt;
|
|
27797
27939
|
disownCapturesStmt;
|
|
27798
27940
|
partitionStmt;
|
|
@@ -27856,6 +27998,23 @@ var SqliteHistorySyncRepository = class {
|
|
|
27856
27998
|
markCaptureOwed(id) {
|
|
27857
27999
|
this.markOwedStmt.run({ id });
|
|
27858
28000
|
}
|
|
28001
|
+
/**
|
|
28002
|
+
* Mark every capture already on disk as owed, as of `before`.
|
|
28003
|
+
*
|
|
28004
|
+
* The consent-time backfill, called once from `aka attach` when a human
|
|
28005
|
+
* grants existing-history consent — never from an ongoing drain pass, and
|
|
28006
|
+
* never inferred from a boundary that could later move. `before` is the
|
|
28007
|
+
* caller's own "now" at the moment consent was granted, so what this marks
|
|
28008
|
+
* is exactly the backlog the consent prompt already counted, not whatever a
|
|
28009
|
+
* later re-attach or key rotation might widen it to.
|
|
28010
|
+
*
|
|
28011
|
+
* Returns how many rows matched, for the caller to log or test against. Not a
|
|
28012
|
+
* count of NEWLY marked rows — a row still unsynced from an earlier call
|
|
28013
|
+
* matches again and is counted again, the same as `UPDATE`'s own `changes`.
|
|
28014
|
+
*/
|
|
28015
|
+
markCaptureBacklogOwed(before) {
|
|
28016
|
+
return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
|
|
28017
|
+
}
|
|
27859
28018
|
/** Record delivery. Called only AFTER the far side has accepted the rows. */
|
|
27860
28019
|
markSynced(ids, atMs) {
|
|
27861
28020
|
this.stampAll(ids, atMs);
|
|
@@ -27972,15 +28131,42 @@ var SqliteHistorySyncRepository = class {
|
|
|
27972
28131
|
*
|
|
27973
28132
|
* Delivery is a fact about ONE recipient: rows sent to the deployment a
|
|
27974
28133
|
* machine has just left are undelivered as far as the new one is concerned.
|
|
27975
|
-
* All
|
|
27976
|
-
* attributed to the wrong deployment,
|
|
28134
|
+
* All four in one transaction, so a crash between them cannot leave stamps
|
|
28135
|
+
* attributed to the wrong deployment, a boundary that belongs to another, or
|
|
28136
|
+
* a disown with no re-mark to follow it.
|
|
27977
28137
|
*
|
|
27978
28138
|
* The boundary is written HERE and only here, which is what freezes it: a
|
|
27979
28139
|
* re-attach to the SAME deployment (a key rotation) leaves the fingerprint
|
|
27980
28140
|
* unchanged, so this never runs and the backlog does not widen back over rows
|
|
27981
28141
|
* the live path has since delivered.
|
|
28142
|
+
*
|
|
28143
|
+
* `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
|
|
28144
|
+
* granted existing-history consent for the deployment this call is arming —
|
|
28145
|
+
* a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
|
|
28146
|
+
* instant, `backlogBefore` is the ATTACH instant, and the two can be far
|
|
28147
|
+
* apart. Passed only when that grant is valid, since this method has no way
|
|
28148
|
+
* to check consent itself and must not mark a row owed for a machine that
|
|
28149
|
+
* never agreed to it. Applied AFTER the disown above, in the SAME
|
|
28150
|
+
* transaction: what the disown clears is every marker below `backlogBefore`,
|
|
28151
|
+
* which includes this deployment's OWN pre-attach rows — `aka attach` calls
|
|
28152
|
+
* `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
|
|
28153
|
+
* on the cleared side of that bound — and the re-mark in the same
|
|
28154
|
+
* transaction is what puts those rows back. A crash between the two cannot
|
|
28155
|
+
* strand the ledger disowned with nothing re-marked — the transaction either
|
|
28156
|
+
* lands whole or not at all, and a fingerprint mismatch that has not yet
|
|
28157
|
+
* committed re-enters this method on the very next pass. Omit it (the
|
|
28158
|
+
* structural-only tests do) to exercise the disown in isolation.
|
|
28159
|
+
*
|
|
28160
|
+
* The disown is bounded by `backlogBefore`, which is what keeps it from
|
|
28161
|
+
* touching a marker the NEW deployment's OWN live path has already set: B's
|
|
28162
|
+
* live path can mark a capture owed from the moment `aka attach` writes the
|
|
28163
|
+
* descriptor, before the drain's first pass ever reaches this method, and
|
|
28164
|
+
* such a row sits at or after the bound rather than below it. What keeps the
|
|
28165
|
+
* disown from eating THIS SAME CALL's own re-mark is the order, not the
|
|
28166
|
+
* bound — disown runs first, re-mark second, both inside the one
|
|
28167
|
+
* transaction above.
|
|
27982
28168
|
*/
|
|
27983
|
-
rearmFor(fingerprint, backlogBefore) {
|
|
28169
|
+
rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
|
|
27984
28170
|
this.ensureRowStmt.run();
|
|
27985
28171
|
withTransaction(
|
|
27986
28172
|
this.db,
|
|
@@ -27988,7 +28174,10 @@ var SqliteHistorySyncRepository = class {
|
|
|
27988
28174
|
const previous = getRow(this.fingerprintStmt)?.fingerprint;
|
|
27989
28175
|
this.rearmStmt.run();
|
|
27990
28176
|
if (previous !== null && previous !== void 0 && previous !== fingerprint) {
|
|
27991
|
-
this.disownCapturesStmt.run();
|
|
28177
|
+
this.disownCapturesStmt.run({ attachedAt: backlogBefore });
|
|
28178
|
+
}
|
|
28179
|
+
if (backfillCapturesBefore !== void 0) {
|
|
28180
|
+
this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
|
|
27992
28181
|
}
|
|
27993
28182
|
this.setFingerprintStmt.run({ fingerprint, backlogBefore });
|
|
27994
28183
|
},
|
|
@@ -28265,7 +28454,8 @@ function managedSettingsPaths(platform2 = process.platform) {
|
|
|
28265
28454
|
}
|
|
28266
28455
|
return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
|
|
28267
28456
|
}
|
|
28268
|
-
|
|
28457
|
+
var testOnlyManagedPaths = null;
|
|
28458
|
+
function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
|
|
28269
28459
|
for (const path of paths) {
|
|
28270
28460
|
let text;
|
|
28271
28461
|
try {
|
|
@@ -30745,7 +30935,7 @@ function toUtcDateString(ms) {
|
|
|
30745
30935
|
return new Date(ms).toISOString().slice(0, 10);
|
|
30746
30936
|
}
|
|
30747
30937
|
function isTimeseriesSeverity(s) {
|
|
30748
|
-
return s === "critical" || s === "high" || s === "medium";
|
|
30938
|
+
return s === "critical" || s === "high" || s === "medium" || s === "low";
|
|
30749
30939
|
}
|
|
30750
30940
|
var SqliteSecurityRepository = class {
|
|
30751
30941
|
constructor(db, now = () => Date.now()) {
|
|
@@ -30874,12 +31064,16 @@ var SqliteSecurityRepository = class {
|
|
|
30874
31064
|
const now = this.now();
|
|
30875
31065
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
30876
31066
|
const rows = this.findingsInRange(windowStart, now);
|
|
30877
|
-
const points = Array.from(
|
|
30878
|
-
|
|
30879
|
-
|
|
30880
|
-
|
|
30881
|
-
|
|
30882
|
-
|
|
31067
|
+
const points = Array.from(
|
|
31068
|
+
{ length: numBuckets },
|
|
31069
|
+
(_, i) => ({
|
|
31070
|
+
timestamp: toUtcDateString(windowStart + i * bucketMs),
|
|
31071
|
+
critical: 0,
|
|
31072
|
+
high: 0,
|
|
31073
|
+
medium: 0,
|
|
31074
|
+
low: 0
|
|
31075
|
+
})
|
|
31076
|
+
);
|
|
30883
31077
|
for (const r of rows) {
|
|
30884
31078
|
const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
|
|
30885
31079
|
const bucket = points[idx];
|
|
@@ -31097,6 +31291,7 @@ var SqliteSecurityRepository = class {
|
|
|
31097
31291
|
`SELECT f.finding_key AS finding_key,
|
|
31098
31292
|
d.rule_id AS rule_id,
|
|
31099
31293
|
d.severity AS severity,
|
|
31294
|
+
e.repo AS repo,
|
|
31100
31295
|
e.file_path AS path,
|
|
31101
31296
|
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
31102
31297
|
latest.resolved_at AS latest_resolved_at
|
|
@@ -31116,6 +31311,7 @@ var SqliteSecurityRepository = class {
|
|
|
31116
31311
|
const items = rows.map((r) => ({
|
|
31117
31312
|
findingKey: r.finding_key,
|
|
31118
31313
|
ruleId: r.rule_id,
|
|
31314
|
+
repo: r.repo ?? "",
|
|
31119
31315
|
severity: r.severity,
|
|
31120
31316
|
path: r.path ?? "",
|
|
31121
31317
|
resolvedAt: new Date(r.latest_resolved_at).toISOString(),
|
|
@@ -31125,13 +31321,66 @@ var SqliteSecurityRepository = class {
|
|
|
31125
31321
|
}));
|
|
31126
31322
|
return Promise.resolve({ items });
|
|
31127
31323
|
}
|
|
31324
|
+
/**
|
|
31325
|
+
* Per-rule tallies of the findings that are still OPEN, whole-store.
|
|
31326
|
+
*
|
|
31327
|
+
* Scoped by status rather than by time, because the card this feeds is a to-do
|
|
31328
|
+
* list: a secret committed three weeks ago and never rotated is still the most
|
|
31329
|
+
* important thing to fix, and any window hides it. It carried a "newest N
|
|
31330
|
+
* findings" cap and then a range; the first meant a different span on every
|
|
31331
|
+
* machine, and the second reported "no recommendations" over live exposure.
|
|
31332
|
+
*
|
|
31333
|
+
* `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
|
|
31334
|
+
* so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
|
|
31335
|
+
* is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
|
|
31336
|
+
* dismissal is a judgement, not a remediation) and drops untracked legacy rows.
|
|
31337
|
+
* The two answer different questions and only this one has to match a link.
|
|
31338
|
+
*
|
|
31339
|
+
* Aggregated in SQL: the result is O(distinct rule × category × severity), so a
|
|
31340
|
+
* whole-store scope costs a grouped scan rather than a row per finding.
|
|
31341
|
+
*/
|
|
31342
|
+
recommendationInputs() {
|
|
31343
|
+
const rows = allRows(
|
|
31344
|
+
this.db.prepare(
|
|
31345
|
+
`SELECT d.rule_id AS rule_id,
|
|
31346
|
+
d.category AS category,
|
|
31347
|
+
d.severity AS severity,
|
|
31348
|
+
COUNT(*) AS count
|
|
31349
|
+
FROM inspection_findings f
|
|
31350
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31351
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
31352
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
31353
|
+
ON latest.finding_key = f.finding_key
|
|
31354
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
31355
|
+
AND e.event_type = 'code_change'
|
|
31356
|
+
AND (
|
|
31357
|
+
f.finding_key IS NULL
|
|
31358
|
+
OR latest.status IS NULL
|
|
31359
|
+
OR latest.status NOT IN ('resolved', 'dismissed')
|
|
31360
|
+
)
|
|
31361
|
+
GROUP BY d.rule_id, d.category, d.severity`
|
|
31362
|
+
)
|
|
31363
|
+
);
|
|
31364
|
+
return Promise.resolve(
|
|
31365
|
+
rows.map((r) => ({
|
|
31366
|
+
ruleId: r.rule_id,
|
|
31367
|
+
category: r.category,
|
|
31368
|
+
severity: r.severity,
|
|
31369
|
+
count: r.count
|
|
31370
|
+
}))
|
|
31371
|
+
);
|
|
31372
|
+
}
|
|
31128
31373
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
31129
31374
|
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
31130
31375
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
31131
31376
|
findingsInRange(fromMs, toMs) {
|
|
31132
31377
|
const rows = allRows(
|
|
31133
31378
|
this.db.prepare(
|
|
31134
|
-
`
|
|
31379
|
+
// `rule_id`/`category` cost nothing extra: inspection_definitions is already
|
|
31380
|
+
// joined for `severity`, so they are two more columns off a row this read
|
|
31381
|
+
// already fetches. They feed the recommended-actions rollup.
|
|
31382
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
|
|
31383
|
+
d.rule_id AS rule_id, d.category AS category
|
|
31135
31384
|
FROM inspection_findings f
|
|
31136
31385
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31137
31386
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -31144,7 +31393,9 @@ var SqliteSecurityRepository = class {
|
|
|
31144
31393
|
return rows.map((r) => ({
|
|
31145
31394
|
occurredAt: r.occurred_at,
|
|
31146
31395
|
severity: r.severity,
|
|
31147
|
-
actionTaken: r.action_taken
|
|
31396
|
+
actionTaken: r.action_taken,
|
|
31397
|
+
ruleId: r.rule_id,
|
|
31398
|
+
category: r.category
|
|
31148
31399
|
}));
|
|
31149
31400
|
}
|
|
31150
31401
|
};
|
|
@@ -32314,8 +32565,45 @@ function openLocalDatabase(dir) {
|
|
|
32314
32565
|
};
|
|
32315
32566
|
}
|
|
32316
32567
|
|
|
32317
|
-
// ../../packages/persistence/src/
|
|
32568
|
+
// ../../packages/persistence/src/egress-wire.ts
|
|
32318
32569
|
import { createHash as createHash3 } from "crypto";
|
|
32570
|
+
function hashProjectKey(projectKey) {
|
|
32571
|
+
return createHash3("sha256").update(projectKey, "utf8").digest("hex");
|
|
32572
|
+
}
|
|
32573
|
+
function toIngestHit(hit) {
|
|
32574
|
+
return {
|
|
32575
|
+
host: hit.host,
|
|
32576
|
+
kind: hit.kind,
|
|
32577
|
+
name: hit.name,
|
|
32578
|
+
category: hit.category,
|
|
32579
|
+
trust: hit.trust,
|
|
32580
|
+
network: hit.network,
|
|
32581
|
+
method: hit.method,
|
|
32582
|
+
transport: hit.transport,
|
|
32583
|
+
url: hit.url,
|
|
32584
|
+
template: hit.template,
|
|
32585
|
+
dataClass: hit.dataClass,
|
|
32586
|
+
site: {
|
|
32587
|
+
file: hit.site.file,
|
|
32588
|
+
line: hit.site.line,
|
|
32589
|
+
dynamic: hit.site.dynamic,
|
|
32590
|
+
vendored: hit.site.vendored
|
|
32591
|
+
}
|
|
32592
|
+
};
|
|
32593
|
+
}
|
|
32594
|
+
function toEgressIngestRequest(input2) {
|
|
32595
|
+
const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
|
|
32596
|
+
const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
|
|
32597
|
+
return {
|
|
32598
|
+
projectKey: hashProjectKey(input2.projectKey),
|
|
32599
|
+
project: input2.project,
|
|
32600
|
+
reconcile,
|
|
32601
|
+
hits: hits.map(toIngestHit)
|
|
32602
|
+
};
|
|
32603
|
+
}
|
|
32604
|
+
|
|
32605
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
32606
|
+
import { createHash as createHash4 } from "crypto";
|
|
32319
32607
|
|
|
32320
32608
|
// ../../packages/persistence/src/fingerprint.ts
|
|
32321
32609
|
import { createHmac, randomBytes } from "crypto";
|
|
@@ -32356,14 +32644,18 @@ function readFingerprintKey(dataDir2) {
|
|
|
32356
32644
|
return parseKeyFile(raw);
|
|
32357
32645
|
}
|
|
32358
32646
|
|
|
32359
|
-
// ../../packages/persistence/src/history-
|
|
32647
|
+
// ../../packages/persistence/src/history-backfill.ts
|
|
32360
32648
|
import { existsSync as existsSync4 } from "fs";
|
|
32361
32649
|
import { join as join9 } from "path";
|
|
32650
|
+
|
|
32651
|
+
// ../../packages/persistence/src/history-preview.ts
|
|
32652
|
+
import { existsSync as existsSync5 } from "fs";
|
|
32653
|
+
import { join as join10 } from "path";
|
|
32362
32654
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
32363
32655
|
|
|
32364
32656
|
// ../../packages/persistence/src/store-symlinks.ts
|
|
32365
|
-
import { existsSync as
|
|
32366
|
-
import { dirname as dirname3, join as
|
|
32657
|
+
import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
|
|
32658
|
+
import { dirname as dirname3, join as join11, resolve } from "path";
|
|
32367
32659
|
|
|
32368
32660
|
// ../../packages/persistence/src/vault/crypto.ts
|
|
32369
32661
|
import {
|
|
@@ -32378,62 +32670,25 @@ import {
|
|
|
32378
32670
|
import { execFileSync } from "child_process";
|
|
32379
32671
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
32380
32672
|
import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
32381
|
-
import { join as
|
|
32673
|
+
import { join as join12 } from "path";
|
|
32382
32674
|
|
|
32383
32675
|
// ../../packages/persistence/src/vault/vault.ts
|
|
32384
32676
|
import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
|
|
32385
32677
|
|
|
32386
32678
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
32387
|
-
import { existsSync as
|
|
32388
|
-
import { join as
|
|
32679
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
32680
|
+
import { join as join13 } from "path";
|
|
32389
32681
|
var MARKER = "warn-era-capped";
|
|
32390
32682
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
32391
32683
|
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
32392
|
-
const marker =
|
|
32393
|
-
if (
|
|
32684
|
+
const marker = join13(dataDir2, MARKER);
|
|
32685
|
+
if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
|
|
32394
32686
|
const capped = db.policies.capCategoryActions();
|
|
32395
32687
|
writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
|
|
32396
32688
|
`, { mode: DATA_FILE_MODE });
|
|
32397
32689
|
return { capped };
|
|
32398
32690
|
}
|
|
32399
32691
|
|
|
32400
|
-
// ../../packages/plugin-runtime/src/attached/egress-wire.ts
|
|
32401
|
-
import { createHash as createHash4 } from "crypto";
|
|
32402
|
-
function hashProjectKey(projectKey) {
|
|
32403
|
-
return createHash4("sha256").update(projectKey, "utf8").digest("hex");
|
|
32404
|
-
}
|
|
32405
|
-
function toIngestHit(hit) {
|
|
32406
|
-
return {
|
|
32407
|
-
host: hit.host,
|
|
32408
|
-
kind: hit.kind,
|
|
32409
|
-
name: hit.name,
|
|
32410
|
-
category: hit.category,
|
|
32411
|
-
trust: hit.trust,
|
|
32412
|
-
network: hit.network,
|
|
32413
|
-
method: hit.method,
|
|
32414
|
-
transport: hit.transport,
|
|
32415
|
-
url: hit.url,
|
|
32416
|
-
template: hit.template,
|
|
32417
|
-
dataClass: hit.dataClass,
|
|
32418
|
-
site: {
|
|
32419
|
-
file: hit.site.file,
|
|
32420
|
-
line: hit.site.line,
|
|
32421
|
-
dynamic: hit.site.dynamic,
|
|
32422
|
-
vendored: hit.site.vendored
|
|
32423
|
-
}
|
|
32424
|
-
};
|
|
32425
|
-
}
|
|
32426
|
-
function toEgressIngestRequest(input2) {
|
|
32427
|
-
const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
|
|
32428
|
-
const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
|
|
32429
|
-
return {
|
|
32430
|
-
projectKey: hashProjectKey(input2.projectKey),
|
|
32431
|
-
project: input2.project,
|
|
32432
|
-
reconcile,
|
|
32433
|
-
hits: hits.map(toIngestHit)
|
|
32434
|
-
};
|
|
32435
|
-
}
|
|
32436
|
-
|
|
32437
32692
|
// ../../packages/remote/src/http.ts
|
|
32438
32693
|
import { request as httpRequest } from "http";
|
|
32439
32694
|
import { request as httpsRequest } from "https";
|
|
@@ -32713,6 +32968,7 @@ function createRemoteClient(options) {
|
|
|
32713
32968
|
url: url2(ROUTES.shares),
|
|
32714
32969
|
body: JSON.stringify(validated.data)
|
|
32715
32970
|
});
|
|
32971
|
+
if (response.status === 404) throw new RemoteRouteAbsent(ROUTES.shares);
|
|
32716
32972
|
okBody(response);
|
|
32717
32973
|
},
|
|
32718
32974
|
async pollCommand() {
|
|
@@ -32735,19 +32991,51 @@ function createRemoteClient(options) {
|
|
|
32735
32991
|
};
|
|
32736
32992
|
}
|
|
32737
32993
|
|
|
32738
|
-
// ../../packages/
|
|
32994
|
+
// ../../packages/remote/src/failure-kind.ts
|
|
32739
32995
|
function statusOf(err) {
|
|
32740
32996
|
if (typeof err !== "object" || err === null || !("status" in err)) return null;
|
|
32741
32997
|
const { status } = err;
|
|
32742
32998
|
if (typeof status !== "number" || !Number.isInteger(status)) return null;
|
|
32743
32999
|
return status >= 100 && status <= 599 ? status : null;
|
|
32744
33000
|
}
|
|
32745
|
-
function
|
|
32746
|
-
|
|
33001
|
+
function nameOf(err) {
|
|
33002
|
+
if (typeof err !== "object" || err === null || !("name" in err)) return null;
|
|
33003
|
+
return typeof err.name === "string" ? err.name : null;
|
|
33004
|
+
}
|
|
33005
|
+
function classifyRemoteFailure(err) {
|
|
33006
|
+
switch (nameOf(err)) {
|
|
33007
|
+
case "RemoteRouteAbsent":
|
|
33008
|
+
return "route-absent";
|
|
33009
|
+
case "RemoteRequestInvalid":
|
|
33010
|
+
return "invalid-request";
|
|
33011
|
+
case "RemoteResponseInvalid":
|
|
33012
|
+
return "rejected";
|
|
33013
|
+
default:
|
|
33014
|
+
break;
|
|
33015
|
+
}
|
|
33016
|
+
const status = statusOf(err);
|
|
33017
|
+
if (status === null) return "unreachable";
|
|
33018
|
+
switch (status) {
|
|
32747
33019
|
case 401:
|
|
32748
33020
|
return "unauthorized";
|
|
32749
33021
|
case 403:
|
|
32750
33022
|
return "forbidden";
|
|
33023
|
+
case 429:
|
|
33024
|
+
return "unreachable";
|
|
33025
|
+
case 404:
|
|
33026
|
+
return "unreachable";
|
|
33027
|
+
default:
|
|
33028
|
+
return status >= 400 && status <= 499 ? "rejected" : "unreachable";
|
|
33029
|
+
}
|
|
33030
|
+
}
|
|
33031
|
+
|
|
33032
|
+
// ../../packages/plugin-runtime/src/attached/failure.ts
|
|
33033
|
+
function classifyFailure(err) {
|
|
33034
|
+
switch (classifyRemoteFailure(err)) {
|
|
33035
|
+
case "unauthorized":
|
|
33036
|
+
return "unauthorized";
|
|
33037
|
+
case "forbidden":
|
|
33038
|
+
return "forbidden";
|
|
32751
33039
|
default:
|
|
32752
33040
|
return "unreachable";
|
|
32753
33041
|
}
|
|
@@ -32770,10 +33058,10 @@ function withTimeout(promise2, ms) {
|
|
|
32770
33058
|
|
|
32771
33059
|
// ../../packages/plugin-runtime/src/attached/forward-drops.ts
|
|
32772
33060
|
import { readFileSync as readFileSync8 } from "fs";
|
|
32773
|
-
import { join as
|
|
33061
|
+
import { join as join14 } from "path";
|
|
32774
33062
|
var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
|
|
32775
33063
|
function forwardDropsPath(dataDir2) {
|
|
32776
|
-
return
|
|
33064
|
+
return join14(dataDir2, FORWARD_DROPS_FILENAME);
|
|
32777
33065
|
}
|
|
32778
33066
|
function recordForwardDrops(dataDir2, count, nowMs) {
|
|
32779
33067
|
if (count <= 0) return;
|
|
@@ -32809,13 +33097,13 @@ function readForwardDrops(dataDir2) {
|
|
|
32809
33097
|
|
|
32810
33098
|
// ../../packages/plugin-runtime/src/attached/forward-policy.ts
|
|
32811
33099
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
32812
|
-
import { readFileSync as
|
|
33100
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
32813
33101
|
import { readFile, rename, writeFile } from "fs/promises";
|
|
32814
|
-
import { join as
|
|
33102
|
+
import { join as join24 } from "path";
|
|
32815
33103
|
|
|
32816
33104
|
// ../../packages/plugin-sdk/src/config.ts
|
|
32817
|
-
import { existsSync as
|
|
32818
|
-
import { join as
|
|
33105
|
+
import { existsSync as existsSync8 } from "fs";
|
|
33106
|
+
import { join as join15 } from "path";
|
|
32819
33107
|
|
|
32820
33108
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
32821
33109
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
@@ -32869,8 +33157,8 @@ function resolveProvider() {
|
|
|
32869
33157
|
function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
|
|
32870
33158
|
try {
|
|
32871
33159
|
ensureLayoutDirSync(base);
|
|
32872
|
-
const settingsFile =
|
|
32873
|
-
if (
|
|
33160
|
+
const settingsFile = join15(settingsDir(base), "settings.json");
|
|
33161
|
+
if (existsSync8(settingsFile)) tightenFile(settingsFile);
|
|
32874
33162
|
} catch {
|
|
32875
33163
|
}
|
|
32876
33164
|
migrateLegacyLayout(base);
|
|
@@ -32895,7 +33183,7 @@ function resolveProviderSafe(resolveProviderFn) {
|
|
|
32895
33183
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
32896
33184
|
import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
|
|
32897
33185
|
import { homedir as homedir2 } from "os";
|
|
32898
|
-
import { basename as basename3, join as
|
|
33186
|
+
import { basename as basename3, join as join17 } from "path";
|
|
32899
33187
|
|
|
32900
33188
|
// ../../packages/detections/src/egress/registry.ts
|
|
32901
33189
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -35678,24 +35966,20 @@ function bundledDetections() {
|
|
|
35678
35966
|
}
|
|
35679
35967
|
|
|
35680
35968
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
35681
|
-
import { existsSync as
|
|
35682
|
-
import { basename as basename2, dirname as dirname4, isAbsolute, join as
|
|
35969
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
35970
|
+
import { basename as basename2, dirname as dirname4, isAbsolute, join as join16, sep as sep2 } from "path";
|
|
35683
35971
|
|
|
35684
35972
|
// ../../packages/plugin-sdk/src/events.ts
|
|
35685
35973
|
import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
|
|
35686
35974
|
|
|
35687
35975
|
// ../../packages/plugin-sdk/src/isolated-scan.ts
|
|
35688
|
-
import { existsSync as
|
|
35976
|
+
import { existsSync as existsSync10 } from "fs";
|
|
35689
35977
|
import { fileURLToPath } from "url";
|
|
35690
35978
|
import { Worker } from "worker_threads";
|
|
35691
35979
|
|
|
35692
|
-
// ../../packages/plugin-sdk/src/
|
|
35693
|
-
|
|
35694
|
-
import {
|
|
35695
|
-
import { join as join17 } from "path";
|
|
35696
|
-
|
|
35697
|
-
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
35698
|
-
import { arch, hostname as hostname4, platform, release } from "os";
|
|
35980
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
35981
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
35982
|
+
import { join as join19 } from "path";
|
|
35699
35983
|
|
|
35700
35984
|
// ../../packages/plugin-sdk/src/model-governance.ts
|
|
35701
35985
|
import {
|
|
@@ -35703,24 +35987,50 @@ import {
|
|
|
35703
35987
|
fstatSync,
|
|
35704
35988
|
mkdirSync as mkdirSync2,
|
|
35705
35989
|
openSync as openSync2,
|
|
35706
|
-
readFileSync as
|
|
35990
|
+
readFileSync as readFileSync11,
|
|
35707
35991
|
readSync,
|
|
35708
35992
|
writeFileSync as writeFileSync5
|
|
35709
35993
|
} from "fs";
|
|
35710
35994
|
import { join as join18 } from "path";
|
|
35711
35995
|
var TAIL_BYTES = 256 * 1024;
|
|
35712
35996
|
|
|
35997
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
35998
|
+
var HOST_FEATURE = {
|
|
35999
|
+
ModelSwitch: "model-switch",
|
|
36000
|
+
VaultPointerDisplay: "vault-pointer-display"
|
|
36001
|
+
};
|
|
36002
|
+
var HOST_FLOORS = {
|
|
36003
|
+
[HOST_FEATURE.ModelSwitch]: {
|
|
36004
|
+
label: "model-switch protection",
|
|
36005
|
+
hookEvents: ["PreModelSwitch", "PostModelSwitch"],
|
|
36006
|
+
since: "2.1.251"
|
|
36007
|
+
},
|
|
36008
|
+
[HOST_FEATURE.VaultPointerDisplay]: {
|
|
36009
|
+
label: "vault pointer display",
|
|
36010
|
+
hookEvents: ["MessageDisplay"],
|
|
36011
|
+
since: "2.1.152"
|
|
36012
|
+
}
|
|
36013
|
+
};
|
|
36014
|
+
|
|
36015
|
+
// ../../packages/plugin-sdk/src/ignore-layers.ts
|
|
36016
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
36017
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
36018
|
+
import { join as join20 } from "path";
|
|
36019
|
+
|
|
36020
|
+
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
36021
|
+
import { arch, hostname as hostname4, platform, release } from "os";
|
|
36022
|
+
|
|
35713
36023
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
35714
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
35715
|
-
import { join as
|
|
36024
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
|
|
36025
|
+
import { join as join21 } from "path";
|
|
35716
36026
|
|
|
35717
36027
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
35718
36028
|
import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
|
|
35719
36029
|
import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
|
|
35720
36030
|
|
|
35721
36031
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
35722
|
-
import { existsSync as
|
|
35723
|
-
import { basename as basename5, join as
|
|
36032
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
|
|
36033
|
+
import { basename as basename5, join as join22 } from "path";
|
|
35724
36034
|
|
|
35725
36035
|
// ../../packages/plugin-sdk/src/provider-env-antigravity.ts
|
|
35726
36036
|
var optionalBaseUrl2 = external_exports.preprocess((v) => {
|
|
@@ -35756,7 +36066,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
35756
36066
|
|
|
35757
36067
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
35758
36068
|
import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
35759
|
-
import { join as
|
|
36069
|
+
import { join as join23 } from "path";
|
|
35760
36070
|
|
|
35761
36071
|
// ../../packages/plugin-runtime/src/attached/forward-policy.ts
|
|
35762
36072
|
function isInvalidRequest(err) {
|
|
@@ -35796,7 +36106,7 @@ function parseBreakerState(raw, nowMs) {
|
|
|
35796
36106
|
}
|
|
35797
36107
|
function createForwardPolicy(deps) {
|
|
35798
36108
|
const now = deps.now ?? (() => Date.now());
|
|
35799
|
-
const file2 =
|
|
36109
|
+
const file2 = join24(deps.dir, STATE_FILENAME);
|
|
35800
36110
|
let state = null;
|
|
35801
36111
|
let loading = null;
|
|
35802
36112
|
async function readState() {
|
|
@@ -36518,8 +36828,8 @@ function toolAuditEvent(input2) {
|
|
|
36518
36828
|
}
|
|
36519
36829
|
|
|
36520
36830
|
// ../../packages/plugin-runtime/src/attached/history-state.ts
|
|
36521
|
-
import { readFileSync as
|
|
36522
|
-
import { join as
|
|
36831
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
36832
|
+
import { join as join25 } from "path";
|
|
36523
36833
|
|
|
36524
36834
|
// ../../packages/plugin-runtime/src/attached/history-sync.ts
|
|
36525
36835
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -36536,7 +36846,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
36536
36846
|
var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
|
|
36537
36847
|
|
|
36538
36848
|
// ../../packages/plugin-runtime/src/attached/plugin-block.ts
|
|
36539
|
-
import { readFileSync as
|
|
36849
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
36540
36850
|
function createPluginBlock(build, policyStore) {
|
|
36541
36851
|
return async () => {
|
|
36542
36852
|
const cached2 = await policyStore.read();
|
|
@@ -36555,7 +36865,7 @@ function createPluginBlock(build, policyStore) {
|
|
|
36555
36865
|
// ../../packages/plugin-runtime/src/attached/policy-store.ts
|
|
36556
36866
|
import { randomUUID as randomUUID16 } from "crypto";
|
|
36557
36867
|
import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
36558
|
-
import { join as
|
|
36868
|
+
import { join as join26 } from "path";
|
|
36559
36869
|
|
|
36560
36870
|
// ../../packages/plugin-runtime/src/attached/atomic-publish.ts
|
|
36561
36871
|
import { rename as rename2 } from "fs/promises";
|
|
@@ -36579,7 +36889,7 @@ async function publishByRename(tmp, file2, move = rename2) {
|
|
|
36579
36889
|
|
|
36580
36890
|
// ../../packages/plugin-runtime/src/attached/policy-store.ts
|
|
36581
36891
|
function createPolicyStore(dir = dataDir()) {
|
|
36582
|
-
const file2 =
|
|
36892
|
+
const file2 = join26(dir, "policy-cache.json");
|
|
36583
36893
|
async function read() {
|
|
36584
36894
|
try {
|
|
36585
36895
|
const raw = await readFile2(file2, "utf8");
|
|
@@ -36810,11 +37120,11 @@ function readStorePosture(dbPath2) {
|
|
|
36810
37120
|
// ../../packages/plugin-runtime/src/attached/posture-store.ts
|
|
36811
37121
|
import { randomUUID as randomUUID17 } from "crypto";
|
|
36812
37122
|
import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
36813
|
-
import { join as
|
|
37123
|
+
import { join as join27 } from "path";
|
|
36814
37124
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
36815
37125
|
function createPostureStore(dir = settingsDir(), legacyDir) {
|
|
36816
|
-
const file2 =
|
|
36817
|
-
const legacyFile = legacyDir === void 0 ? null :
|
|
37126
|
+
const file2 = join27(dir, "posture-state.json");
|
|
37127
|
+
const legacyFile = legacyDir === void 0 ? null : join27(legacyDir, "posture-state.json");
|
|
36818
37128
|
async function persist(state) {
|
|
36819
37129
|
await ensureDataDir(dir);
|
|
36820
37130
|
const tmp = `${file2}.${randomUUID17()}.tmp`;
|
|
@@ -36882,8 +37192,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
|
|
|
36882
37192
|
}
|
|
36883
37193
|
|
|
36884
37194
|
// ../../packages/plugin-runtime/src/attached/sync-state.ts
|
|
36885
|
-
import { readFileSync as
|
|
36886
|
-
import { join as
|
|
37195
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
37196
|
+
import { join as join28 } from "path";
|
|
36887
37197
|
|
|
36888
37198
|
// ../../packages/plugin-runtime/src/attached/status.ts
|
|
36889
37199
|
var REFUSAL_LINES = {
|
|
@@ -37467,7 +37777,7 @@ function show(body) {
|
|
|
37467
37777
|
|
|
37468
37778
|
// ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
|
|
37469
37779
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
37470
|
-
import { join as
|
|
37780
|
+
import { join as join29 } from "path";
|
|
37471
37781
|
|
|
37472
37782
|
// ../../packages/setup-wizard/src/triage/merge.ts
|
|
37473
37783
|
var RANK = Object.fromEntries(
|
|
@@ -37475,9 +37785,9 @@ var RANK = Object.fromEntries(
|
|
|
37475
37785
|
);
|
|
37476
37786
|
|
|
37477
37787
|
// ../../packages/setup-wizard/src/triage/plan-file.ts
|
|
37478
|
-
import { mkdtempSync, readFileSync as
|
|
37788
|
+
import { mkdtempSync, readFileSync as readFileSync19, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
37479
37789
|
import { tmpdir } from "os";
|
|
37480
|
-
import { basename as basename6, dirname as dirname6, join as
|
|
37790
|
+
import { basename as basename6, dirname as dirname6, join as join30 } from "path";
|
|
37481
37791
|
var SuppressionEntrySchema = external_exports.object({
|
|
37482
37792
|
ruleId: external_exports.string(),
|
|
37483
37793
|
category: DetectionCategory,
|
|
@@ -37520,7 +37830,6 @@ var PersistedPlanSchema = external_exports.object({
|
|
|
37520
37830
|
|
|
37521
37831
|
// src/render.ts
|
|
37522
37832
|
var STORE_UNAVAILABLE_NOTE = "I couldn't check my records just now \u2014 we can check again soon. Your Claude session keeps going, and I'll fill in as you work.";
|
|
37523
|
-
var SEVERITY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
37524
37833
|
var SEVERITY_GLYPH = {
|
|
37525
37834
|
critical: SHADE.full,
|
|
37526
37835
|
high: SHADE.dark,
|
|
@@ -37530,15 +37839,6 @@ var SEVERITY_GLYPH = {
|
|
|
37530
37839
|
function severityGlyph(severity) {
|
|
37531
37840
|
return SEVERITY_GLYPH[severity] ?? SHADE.light;
|
|
37532
37841
|
}
|
|
37533
|
-
var ADVICE = {
|
|
37534
|
-
secret: "Rotate the exposed credentials and move them out of prompts (secrets manager / env vars).",
|
|
37535
|
-
pii: "Remove or mask personal data before it reaches the model.",
|
|
37536
|
-
financial: "Strip card and account numbers; share only non-sensitive references.",
|
|
37537
|
-
phi: "Remove protected health information \u2014 it should never reach an external model.",
|
|
37538
|
-
code_context: "Confirm this proprietary code context is safe to share.",
|
|
37539
|
-
code_flaw: "Review the flagged pattern and apply the secure alternative (parameterized queries, safe deserializers, etc.).",
|
|
37540
|
-
custom: "Review against your organization\u2019s custom policy."
|
|
37541
|
-
};
|
|
37542
37842
|
var ACTION_LABEL = {
|
|
37543
37843
|
log: "monitor",
|
|
37544
37844
|
warn: "warn",
|
|
@@ -37556,15 +37856,10 @@ function renderPosture(rows) {
|
|
|
37556
37856
|
return [...rows].sort((a, b) => categoryRank(a.category) - categoryRank(b.category)).map((r) => ` ${r.category.padEnd(width)} ${ACTION_LABEL[r.action] ?? r.action}`).join("\n");
|
|
37557
37857
|
}
|
|
37558
37858
|
var RULE_WIDTH = 64;
|
|
37559
|
-
function healthScore(summary) {
|
|
37560
|
-
const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
|
|
37561
|
-
const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
|
|
37562
|
-
return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
|
|
37563
|
-
}
|
|
37564
37859
|
var TRY_COMMANDS = ["/aka:dashboard", "/aka:scan"];
|
|
37565
37860
|
function topFindings(findings, limit = 10) {
|
|
37566
37861
|
return [...findings].sort((a, b) => {
|
|
37567
|
-
const sev = (
|
|
37862
|
+
const sev = severityWeight(b.severity) - severityWeight(a.severity);
|
|
37568
37863
|
return sev !== 0 ? sev : b.occurredAt.localeCompare(a.occurredAt);
|
|
37569
37864
|
}).slice(0, limit);
|
|
37570
37865
|
}
|
|
@@ -37624,45 +37919,6 @@ function renderFirstRun(s, registry2) {
|
|
|
37624
37919
|
}
|
|
37625
37920
|
return lines.join("\n");
|
|
37626
37921
|
}
|
|
37627
|
-
var REC_TEMPLATE = {
|
|
37628
|
-
secret: { title: "Exposed secret detected", action: "Rotate" },
|
|
37629
|
-
pii: { title: "Personal data in a prompt", action: "Remove" },
|
|
37630
|
-
financial: { title: "Financial data detected", action: "Strip" },
|
|
37631
|
-
phi: { title: "Health information detected", action: "Remove" },
|
|
37632
|
-
code_context: { title: "Proprietary code shared", action: "Review" },
|
|
37633
|
-
custom: { title: "Custom policy match", action: "Review" }
|
|
37634
|
-
};
|
|
37635
|
-
var MAX_RECOMMENDATIONS = 10;
|
|
37636
|
-
function buildRecommendations(findings) {
|
|
37637
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
37638
|
-
for (const f of findings) {
|
|
37639
|
-
const b = buckets.get(f.category) ?? {
|
|
37640
|
-
category: f.category,
|
|
37641
|
-
count: 0,
|
|
37642
|
-
severity: f.severity,
|
|
37643
|
-
weight: 0,
|
|
37644
|
-
ruleId: f.ruleId
|
|
37645
|
-
};
|
|
37646
|
-
b.count++;
|
|
37647
|
-
const w = SEVERITY_WEIGHT[f.severity] ?? 0;
|
|
37648
|
-
if (w > b.weight) {
|
|
37649
|
-
b.weight = w;
|
|
37650
|
-
b.severity = f.severity;
|
|
37651
|
-
b.ruleId = f.ruleId;
|
|
37652
|
-
}
|
|
37653
|
-
buckets.set(f.category, b);
|
|
37654
|
-
}
|
|
37655
|
-
return [...buckets.values()].sort((a, b) => b.weight - a.weight || b.count - a.count).slice(0, MAX_RECOMMENDATIONS).map((b) => {
|
|
37656
|
-
const t = REC_TEMPLATE[b.category] ?? { title: `${b.category} finding`, action: "Review" };
|
|
37657
|
-
return {
|
|
37658
|
-
severity: b.severity,
|
|
37659
|
-
title: t.title,
|
|
37660
|
-
description: ADVICE[b.category] ?? "Review this finding against your policy.",
|
|
37661
|
-
context: `${b.ruleId} \xB7 ${String(b.count)} finding${b.count === 1 ? "" : "s"}`,
|
|
37662
|
-
action: t.action
|
|
37663
|
-
};
|
|
37664
|
-
});
|
|
37665
|
-
}
|
|
37666
37922
|
|
|
37667
37923
|
// src/firstrun-core.ts
|
|
37668
37924
|
function parseSurfacedCount(argv) {
|