@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
|
@@ -492,9 +492,9 @@ var require_ignore = __commonJS({
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// src/apply-suppressions.ts
|
|
495
|
-
import { existsSync as
|
|
495
|
+
import { existsSync as existsSync12, readFileSync as readFileSync16 } from "fs";
|
|
496
496
|
import { userInfo } from "os";
|
|
497
|
-
import { dirname as dirname8, join as
|
|
497
|
+
import { dirname as dirname8, join as join26 } from "path";
|
|
498
498
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
499
499
|
|
|
500
500
|
// ../../packages/persistence/src/attached-derived.ts
|
|
@@ -20743,13 +20743,11 @@ var FindingGroup = external_exports.object({
|
|
|
20743
20743
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20744
20744
|
instances: external_exports.array(FindingInstance),
|
|
20745
20745
|
// Derived from instances' statuses with open-dominates precedence (see
|
|
20746
|
-
//
|
|
20746
|
+
// foldGroupStatus). Undefined only when no instance carries a status.
|
|
20747
20747
|
status: FindingStatus.optional(),
|
|
20748
|
-
// The distinct people across the WHOLE group, not just the
|
|
20749
|
-
//
|
|
20750
|
-
//
|
|
20751
|
-
// instance carries a user, or when the store supplied whole-group folds
|
|
20752
|
-
// without one.
|
|
20748
|
+
// The distinct people across the WHOLE group, not just the instances
|
|
20749
|
+
// carried here. Undefined when no instance carries a user, or when the
|
|
20750
|
+
// store supplied whole-group folds without one.
|
|
20753
20751
|
users: external_exports.array(FindingUser).optional()
|
|
20754
20752
|
}).meta({ id: "FindingGroup" });
|
|
20755
20753
|
var FindingStats = external_exports.object({
|
|
@@ -20778,21 +20776,31 @@ var FindingFacets = external_exports.object({
|
|
|
20778
20776
|
// counted under no value.
|
|
20779
20777
|
status: external_exports.array(FindingFacetItem),
|
|
20780
20778
|
// Host tool (attributes.tool_name). Present only on the instance-level
|
|
20781
|
-
// reads, which can filter by it; the
|
|
20779
|
+
// reads, which can filter by it; the type-level read omits the dimension
|
|
20782
20780
|
// because a group spans tools.
|
|
20783
20781
|
tool: external_exports.array(FindingFacetItem).optional()
|
|
20784
20782
|
}).meta({ id: "FindingFacets" });
|
|
20785
|
-
var
|
|
20786
|
-
|
|
20783
|
+
var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
|
|
20784
|
+
id: "FindingTypeSummary"
|
|
20785
|
+
});
|
|
20786
|
+
var DEFAULT_FINDING_TYPES_LIMIT = 50;
|
|
20787
|
+
var MAX_FINDING_TYPES_LIMIT = 100;
|
|
20788
|
+
var ListFindingTypesQuery = external_exports.object({
|
|
20787
20789
|
// NOTE: severity filters by Severity (critical/high/medium/low), not by
|
|
20788
|
-
// FindingAction.
|
|
20790
|
+
// FindingAction. It narrows TYPES: a type's severity is the one its newest
|
|
20791
|
+
// firing version carries, and this list pages types.
|
|
20792
|
+
//
|
|
20793
|
+
// That is NOT a claim the findings of a type share it. A rule can hold several
|
|
20794
|
+
// definition versions at different severities, so a type kept by this filter
|
|
20795
|
+
// can hold findings that individually do not match — see totals.findings on
|
|
20796
|
+
// ListFindingTypesResponse, which counts them all.
|
|
20789
20797
|
severity: external_exports.array(Severity).optional(),
|
|
20790
20798
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20791
20799
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20792
20800
|
action: external_exports.array(FindingAction).optional(),
|
|
20793
|
-
// Matches a
|
|
20794
|
-
// individual
|
|
20795
|
-
//
|
|
20801
|
+
// Matches a type's DERIVED status (see FindingGroup.status), not its
|
|
20802
|
+
// individual findings' — so a filtered row's status always reads one of the
|
|
20803
|
+
// requested values.
|
|
20796
20804
|
status: external_exports.array(FindingStatus).optional(),
|
|
20797
20805
|
q: external_exports.string().optional(),
|
|
20798
20806
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
@@ -20802,23 +20810,37 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
20802
20810
|
// from a time-scoped page (Activity's range) can carry that scope. Absent
|
|
20803
20811
|
// means all time — this list has no default window.
|
|
20804
20812
|
from: external_exports.iso.datetime().optional(),
|
|
20805
|
-
// A
|
|
20806
|
-
//
|
|
20807
|
-
//
|
|
20808
|
-
//
|
|
20809
|
-
//
|
|
20813
|
+
// A RULE id that must appear in the page even when the cursor has already
|
|
20814
|
+
// advanced past its sort position. This is what keeps the selected type
|
|
20815
|
+
// visible in the list once it paginates: the target is appended out of sort
|
|
20816
|
+
// order rather than scanned forward for. Never affects totals, facets or the
|
|
20817
|
+
// cursor. Unlike the grouped read this replaces, it names a rule only — an
|
|
20818
|
+
// instance id is resolved by `findingInstance`, which is a primary-key seek
|
|
20819
|
+
// and so is not bounded by what any page happens to hold.
|
|
20810
20820
|
includeId: external_exports.string().optional(),
|
|
20811
|
-
|
|
20812
|
-
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
20821
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
|
|
20813
20822
|
cursor: external_exports.string().optional()
|
|
20814
20823
|
});
|
|
20815
|
-
var
|
|
20824
|
+
var ListFindingTypesResponse = external_exports.object({
|
|
20816
20825
|
totals: external_exports.object({
|
|
20826
|
+
// Findings belonging to the matching TYPES — not findings that each match
|
|
20827
|
+
// the filters. The filters here select types, so a type that survives
|
|
20828
|
+
// contributes its whole instanceCount.
|
|
20829
|
+
//
|
|
20830
|
+
// `status` is the one exception, narrowed per finding via
|
|
20831
|
+
// countInstancesByStatus. `severity`, `provider` and `action` are not, so
|
|
20832
|
+
// this can exceed what the instance read reports for the same filters: a
|
|
20833
|
+
// rule whose severity moved between versions is kept on its newest and
|
|
20834
|
+
// still counts its older findings. Narrowing the other three needs
|
|
20835
|
+
// per-dimension counts the aggregate does not carry today.
|
|
20817
20836
|
findings: external_exports.number().int().nonnegative(),
|
|
20818
|
-
|
|
20837
|
+
// Counts TYPES, which is the unit this read pages. The instance read's
|
|
20838
|
+
// own totals count findings; the two deliberately answer different
|
|
20839
|
+
// questions and are never summed.
|
|
20840
|
+
types: external_exports.number().int().nonnegative()
|
|
20819
20841
|
}),
|
|
20820
20842
|
facets: FindingFacets,
|
|
20821
|
-
items: external_exports.array(
|
|
20843
|
+
items: external_exports.array(FindingTypeSummary),
|
|
20822
20844
|
nextCursor: external_exports.string().nullable(),
|
|
20823
20845
|
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
20824
20846
|
// many times that rule fired in the session's persisted transcript. Findings
|
|
@@ -20826,7 +20848,7 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
20826
20848
|
// every firing, so the two numbers legitimately differ — this map lets a
|
|
20827
20849
|
// session-scoped view show both.
|
|
20828
20850
|
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
20829
|
-
}).meta({ id: "
|
|
20851
|
+
}).meta({ id: "ListFindingTypesResponse" });
|
|
20830
20852
|
var ApplyFindingActionRequest = external_exports.object({
|
|
20831
20853
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
20832
20854
|
// it, so it is excluded from the request contract. The mapping helper
|
|
@@ -20856,12 +20878,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
|
|
|
20856
20878
|
var MAX_FLAT_FINDINGS_LIMIT = 200;
|
|
20857
20879
|
var ListFindingInstancesQuery = external_exports.object({
|
|
20858
20880
|
severity: external_exports.array(Severity).optional(),
|
|
20859
|
-
// Rule ids, the same vocabulary the
|
|
20881
|
+
// Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
|
|
20882
|
+
// ONE of them is how the master/detail view scopes its right-hand panel.
|
|
20860
20883
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20861
20884
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20862
20885
|
action: external_exports.array(FindingAction).optional(),
|
|
20863
20886
|
// Matches each instance's OWN derived status (deriveFindingStatus), unlike
|
|
20864
|
-
// the
|
|
20887
|
+
// the types query's type-level fold.
|
|
20865
20888
|
status: external_exports.array(FindingStatus).optional(),
|
|
20866
20889
|
// Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
|
|
20867
20890
|
// where the free-text `q` can only match the rendered "via Bash" label.
|
|
@@ -20878,37 +20901,47 @@ var ListFindingInstancesQuery = external_exports.object({
|
|
|
20878
20901
|
});
|
|
20879
20902
|
var ListFindingInstancesResponse = external_exports.object({
|
|
20880
20903
|
// Instances matching the filters across the whole scope, not just this
|
|
20881
|
-
// page — cursor-independent, like the
|
|
20904
|
+
// page — cursor-independent, like the types list's totals.
|
|
20882
20905
|
totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
|
|
20883
|
-
// Counts in INSTANCES here, where the
|
|
20906
|
+
// Counts in INSTANCES here, where the types response counts types. Each
|
|
20884
20907
|
// dimension still excludes its own filter.
|
|
20885
20908
|
facets: FindingFacets,
|
|
20886
20909
|
items: external_exports.array(FindingInstanceDetail),
|
|
20887
20910
|
nextCursor: external_exports.string().nullable()
|
|
20888
20911
|
}).meta({ id: "ListFindingInstancesResponse" });
|
|
20889
|
-
var
|
|
20890
|
-
//
|
|
20891
|
-
//
|
|
20892
|
-
|
|
20893
|
-
|
|
20894
|
-
|
|
20895
|
-
|
|
20896
|
-
// Folded from the instances' derived statuses with the same
|
|
20897
|
-
// open-dominates precedence a group uses.
|
|
20898
|
-
status: FindingStatus.optional(),
|
|
20899
|
-
// Distinct rules seen at this location, capped — the row shows them as
|
|
20900
|
-
// chips, and the count is what conveys scale.
|
|
20901
|
-
ruleIds: external_exports.array(external_exports.string())
|
|
20902
|
-
}).meta({ id: "FindingLocationFile" });
|
|
20903
|
-
var FindingLocationRepo = external_exports.object({
|
|
20912
|
+
var FindingLocationSummary = external_exports.object({
|
|
20913
|
+
// Opaque, stable, minted from the pair by encodeLocationId. It exists
|
|
20914
|
+
// because a location's identity is two values and a URL param carries one:
|
|
20915
|
+
// `?loc=` names a location the way `?rule=` names a type. Only ever compared
|
|
20916
|
+
// for EQUALITY — the page's selection check, this read's `includeId`, the
|
|
20917
|
+
// client's page dedupe — never decoded, and never a sort key.
|
|
20918
|
+
id: external_exports.string(),
|
|
20904
20919
|
/** Empty when the instances carried no repo attribute. */
|
|
20905
20920
|
repo: external_exports.string(),
|
|
20921
|
+
// Empty when the instances carried no file path (a prompt, or a tool call
|
|
20922
|
+
// with no file attribution). Both halves empty is a real location — usually
|
|
20923
|
+
// the largest one in a store — and is selectable like any other.
|
|
20924
|
+
file: external_exports.string(),
|
|
20906
20925
|
instanceCount: external_exports.number().int().nonnegative(),
|
|
20926
|
+
// The WORST severity present, not the first row's. It is this list's primary
|
|
20927
|
+
// sort key, so it is also what explains why a row is where it is, and it is
|
|
20928
|
+
// how a reader decides what to open without opening everything.
|
|
20907
20929
|
maxSeverity: Severity,
|
|
20908
20930
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20931
|
+
// Folded from the instances' derived statuses with the same open-dominates
|
|
20932
|
+
// precedence a group uses, so it answers "is anything left to do here" and
|
|
20933
|
+
// not much more: a location holding 1 open among 40 resolved reads like one
|
|
20934
|
+
// holding 40 open. That loss is accepted — the panel beside this list
|
|
20935
|
+
// carries each finding's own status, and instanceCount sits next to the
|
|
20936
|
+
// badge.
|
|
20909
20937
|
status: FindingStatus.optional(),
|
|
20910
|
-
|
|
20911
|
-
|
|
20938
|
+
// Every distinct rule seen at this location, UNCAPPED — so the length is a
|
|
20939
|
+
// tally rather than a sample and a row can say how many there are. Bounded
|
|
20940
|
+
// by the ruleset, not by the store. The view bounds what it DISPLAYS.
|
|
20941
|
+
ruleIds: external_exports.array(external_exports.string())
|
|
20942
|
+
}).meta({ id: "FindingLocationSummary" });
|
|
20943
|
+
var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
|
|
20944
|
+
var MAX_FINDING_LOCATIONS_LIMIT = 100;
|
|
20912
20945
|
var ListFindingLocationsQuery = external_exports.object({
|
|
20913
20946
|
severity: external_exports.array(Severity).optional(),
|
|
20914
20947
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
@@ -20921,18 +20954,42 @@ var ListFindingLocationsQuery = external_exports.object({
|
|
|
20921
20954
|
q: external_exports.string().optional(),
|
|
20922
20955
|
sessionId: external_exports.string().optional(),
|
|
20923
20956
|
from: external_exports.iso.datetime().optional(),
|
|
20924
|
-
|
|
20957
|
+
// A LOCATION id (see FindingLocationSummary.id) that must appear in the page
|
|
20958
|
+
// even when the cursor has already advanced past its sort position — the
|
|
20959
|
+
// counterpart of ListFindingTypesQuery.includeId, and needed far more often
|
|
20960
|
+
// here. Selecting a row pushes the URL, which re-renders the server and resets
|
|
20961
|
+
// the client's page cache to page 0; with distinct (repo, file) pairs running
|
|
20962
|
+
// into the thousands, a selection sitting off page 0 is the ordinary case
|
|
20963
|
+
// rather than a deep-link corner. Never affects totals, facets or the cursor.
|
|
20964
|
+
includeId: external_exports.string().optional(),
|
|
20965
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
|
|
20966
|
+
cursor: external_exports.string().optional()
|
|
20925
20967
|
});
|
|
20926
20968
|
var ListFindingLocationsResponse = external_exports.object({
|
|
20927
20969
|
totals: external_exports.object({
|
|
20970
|
+
// Findings matching the filters across the whole scope. Unlike the types
|
|
20971
|
+
// read's same-named field this needs no caveat: the filters here narrow
|
|
20972
|
+
// per finding, so this is the sum of every row's instanceCount.
|
|
20928
20973
|
findings: external_exports.number().int().nonnegative(),
|
|
20929
|
-
|
|
20930
|
-
|
|
20974
|
+
// Counts LOCATIONS, the unit this read pages — the number the paginator
|
|
20975
|
+
// states. The facets beside it count FINDINGS (see below); a surface
|
|
20976
|
+
// showing both says which is which.
|
|
20977
|
+
locations: external_exports.number().int().nonnegative()
|
|
20931
20978
|
}),
|
|
20932
|
-
|
|
20933
|
-
|
|
20934
|
-
|
|
20935
|
-
|
|
20979
|
+
// Counts in FINDINGS, where the types response counts types, each dimension
|
|
20980
|
+
// still excluding its own filter. Deliberately not locations: counting those
|
|
20981
|
+
// needs a set of location keys per dimension per value — memory tracking the
|
|
20982
|
+
// store times the vocabulary, in a read whose scan promises flat memory —
|
|
20983
|
+
// and the cheap per-location version is not an approximation but WRONG. A
|
|
20984
|
+
// location holding {claudecode, block} and {codex, warn} would survive
|
|
20985
|
+
// provider=claudecode AND action=warn, under which no single finding
|
|
20986
|
+
// matches, so the facet would contradict the instanceCount this whole view
|
|
20987
|
+
// rests on. Findings also keep the toolbar in the same unit as the page
|
|
20988
|
+
// tally and the panel it sits above.
|
|
20989
|
+
facets: FindingFacets,
|
|
20990
|
+
/** Sorted by max severity, then most recent, then (repo, file). */
|
|
20991
|
+
items: external_exports.array(FindingLocationSummary),
|
|
20992
|
+
nextCursor: external_exports.string().nullable()
|
|
20936
20993
|
}).meta({ id: "ListFindingLocationsResponse" });
|
|
20937
20994
|
|
|
20938
20995
|
// ../../packages/schema/src/zod/meta.ts
|
|
@@ -22100,6 +22157,14 @@ var ControlPlaneErrorBody = external_exports.object({
|
|
|
22100
22157
|
message: external_exports.string().optional()
|
|
22101
22158
|
}).optional()
|
|
22102
22159
|
});
|
|
22160
|
+
var RemoteFailureKind = external_exports.enum([
|
|
22161
|
+
"unauthorized",
|
|
22162
|
+
"forbidden",
|
|
22163
|
+
"route-absent",
|
|
22164
|
+
"invalid-request",
|
|
22165
|
+
"rejected",
|
|
22166
|
+
"unreachable"
|
|
22167
|
+
]);
|
|
22103
22168
|
var AttachDeviceRequest = external_exports.object({
|
|
22104
22169
|
// This machine's own continuity id, so re-attaching ROTATES the credential
|
|
22105
22170
|
// on one machine record instead of producing a second one. Client-minted
|
|
@@ -22793,139 +22858,62 @@ function deriveFindingStatus(row) {
|
|
|
22793
22858
|
if (row.latestResolutionStatus === "dismissed") return "dismissed";
|
|
22794
22859
|
return "open";
|
|
22795
22860
|
}
|
|
22796
|
-
function distinctUsers(instances) {
|
|
22797
|
-
const seen = /* @__PURE__ */ new Set();
|
|
22798
|
-
const users = [];
|
|
22799
|
-
for (const i of instances) {
|
|
22800
|
-
if (i.user === void 0 || seen.has(i.user.id)) continue;
|
|
22801
|
-
seen.add(i.user.id);
|
|
22802
|
-
users.push(i.user);
|
|
22803
|
-
}
|
|
22804
|
-
return users;
|
|
22805
|
-
}
|
|
22806
22861
|
function sortUsers(users) {
|
|
22807
22862
|
return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
22808
22863
|
}
|
|
22809
|
-
function
|
|
22810
|
-
const overrides = opts.overrides;
|
|
22864
|
+
function buildFindingTypes(aggregates, opts = {}) {
|
|
22811
22865
|
const packNames = opts.packNames;
|
|
22812
|
-
const
|
|
22813
|
-
const
|
|
22814
|
-
|
|
22815
|
-
const
|
|
22816
|
-
|
|
22817
|
-
else byRuleId.set(row.ruleId, [row]);
|
|
22818
|
-
}
|
|
22819
|
-
const groups = [];
|
|
22820
|
-
for (const [ruleId, ruleRows] of byRuleId) {
|
|
22821
|
-
const instances = ruleRows.map((r) => {
|
|
22822
|
-
const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
|
|
22823
|
-
return {
|
|
22824
|
-
id: r.id,
|
|
22825
|
-
provider: toApiProvider(r.sourceTool),
|
|
22826
|
-
repo: r.repo,
|
|
22827
|
-
file: r.file,
|
|
22828
|
-
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
22829
|
-
...r.eventId === void 0 ? {} : { eventId: r.eventId },
|
|
22830
|
-
...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
|
|
22831
|
-
...r.user === void 0 ? {} : { user: r.user },
|
|
22832
|
-
action: toApiAction(effectiveDbAction),
|
|
22833
|
-
detectedAt: r.occurredAt,
|
|
22834
|
-
confidence: r.confidence,
|
|
22835
|
-
status: r.status
|
|
22836
|
-
};
|
|
22837
|
-
});
|
|
22838
|
-
const agg = aggregates?.get(ruleId);
|
|
22839
|
-
const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
|
|
22840
|
-
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
22841
|
-
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
22842
|
-
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
22843
|
-
);
|
|
22844
|
-
const seenProviders = /* @__PURE__ */ new Set();
|
|
22845
|
-
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
22846
|
-
if (seenProviders.has(p)) return false;
|
|
22847
|
-
seenProviders.add(p);
|
|
22848
|
-
return true;
|
|
22849
|
-
});
|
|
22850
|
-
const actionSet = new Set(
|
|
22851
|
-
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
22852
|
-
);
|
|
22866
|
+
const types = [];
|
|
22867
|
+
for (const [ruleId, agg] of aggregates) {
|
|
22868
|
+
const users = sortUsers(agg.users ?? []);
|
|
22869
|
+
const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
|
|
22870
|
+
const actionSet = new Set(agg.actionsTaken.map(toApiAction));
|
|
22853
22871
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
22854
|
-
const
|
|
22855
|
-
const
|
|
22856
|
-
id: ruleId,
|
|
22857
|
-
name: packNames?.get(ruleId) ?? null
|
|
22858
|
-
};
|
|
22859
|
-
const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
|
|
22860
|
-
const policy = { id: `category:${apiCategory}`, name: apiCategory };
|
|
22861
|
-
const match = {
|
|
22862
|
-
maskedValue: ruleRows[0]?.maskedMatch ?? "",
|
|
22863
|
-
contextPrefix: ""
|
|
22864
|
-
// empty (pending privacy review)
|
|
22865
|
-
};
|
|
22866
|
-
const status = foldGroupStatus(
|
|
22867
|
-
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
22868
|
-
);
|
|
22869
|
-
const group = {
|
|
22872
|
+
const apiCategory = toApiCategory(agg.category ?? "custom");
|
|
22873
|
+
const type = {
|
|
22870
22874
|
id: ruleId,
|
|
22871
22875
|
category: apiCategory,
|
|
22872
22876
|
subtype: ruleId,
|
|
22873
22877
|
// human label comes with pack metadata later
|
|
22874
|
-
severity,
|
|
22875
|
-
|
|
22876
|
-
|
|
22877
|
-
|
|
22878
|
-
instanceCount: agg?.instanceCount ?? instances.length,
|
|
22878
|
+
severity: agg.severity ?? "low",
|
|
22879
|
+
detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
|
|
22880
|
+
policy: { id: `category:${apiCategory}`, name: apiCategory },
|
|
22881
|
+
instanceCount: agg.instanceCount,
|
|
22879
22882
|
providers,
|
|
22880
22883
|
aggregateAction,
|
|
22881
|
-
latestDetectedAt,
|
|
22882
|
-
|
|
22883
|
-
status,
|
|
22884
|
+
latestDetectedAt: agg.latestDetectedAt,
|
|
22885
|
+
status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
|
|
22884
22886
|
...users.length > 0 ? { users } : {}
|
|
22885
22887
|
};
|
|
22886
|
-
|
|
22887
|
-
|
|
22888
|
-
|
|
22889
|
-
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
22890
|
-
}
|
|
22888
|
+
actionsCache.set(type, [...actionSet]);
|
|
22889
|
+
if (agg.searchText !== void 0) {
|
|
22890
|
+
haystackCache.set(type, buildHaystack(type, agg.searchText));
|
|
22891
22891
|
}
|
|
22892
|
-
|
|
22892
|
+
types.push(type);
|
|
22893
22893
|
}
|
|
22894
|
-
return
|
|
22894
|
+
return types;
|
|
22895
22895
|
}
|
|
22896
22896
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
22897
|
-
function buildHaystack(
|
|
22897
|
+
function buildHaystack(t, extra) {
|
|
22898
22898
|
return [
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
|
|
22902
|
-
|
|
22903
|
-
|
|
22904
|
-
...g.instances.map((i) => i.repo),
|
|
22905
|
-
...g.instances.map((i) => i.file),
|
|
22906
|
-
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
22907
|
-
...g.instances.map((i) => i.id),
|
|
22908
|
-
// The people: the whole group's list when the store folded one, plus the
|
|
22909
|
-
// preview's own — the two overlap, and a haystack does not mind.
|
|
22910
|
-
...(g.users ?? []).map((u) => u.name),
|
|
22911
|
-
...g.instances.map((i) => i.user?.name ?? ""),
|
|
22899
|
+
t.subtype,
|
|
22900
|
+
t.category,
|
|
22901
|
+
t.policy.name,
|
|
22902
|
+
t.id,
|
|
22903
|
+
...(t.users ?? []).map((u) => u.name),
|
|
22912
22904
|
...extra === void 0 ? [] : [extra]
|
|
22913
22905
|
].join(" ").toLowerCase();
|
|
22914
22906
|
}
|
|
22915
|
-
function
|
|
22916
|
-
const cached2 = haystackCache.get(
|
|
22907
|
+
function typeHaystack(t) {
|
|
22908
|
+
const cached2 = haystackCache.get(t);
|
|
22917
22909
|
if (cached2 !== void 0) return cached2;
|
|
22918
|
-
const haystack = buildHaystack(
|
|
22919
|
-
haystackCache.set(
|
|
22910
|
+
const haystack = buildHaystack(t);
|
|
22911
|
+
haystackCache.set(t, haystack);
|
|
22920
22912
|
return haystack;
|
|
22921
22913
|
}
|
|
22922
22914
|
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
22923
|
-
function
|
|
22924
|
-
|
|
22925
|
-
if (cached2 !== void 0) return cached2;
|
|
22926
|
-
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
22927
|
-
actionsCache.set(g, actions);
|
|
22928
|
-
return actions;
|
|
22915
|
+
function typeActions(t) {
|
|
22916
|
+
return actionsCache.get(t) ?? [];
|
|
22929
22917
|
}
|
|
22930
22918
|
function countInstancesByStatus(statusInputs, statuses) {
|
|
22931
22919
|
const statusSet = new Set(statuses);
|
|
@@ -22936,8 +22924,8 @@ function countInstancesByStatus(statusInputs, statuses) {
|
|
|
22936
22924
|
}
|
|
22937
22925
|
return sum;
|
|
22938
22926
|
}
|
|
22939
|
-
function applyFindingFilters(
|
|
22940
|
-
let filtered =
|
|
22927
|
+
function applyFindingFilters(types, opts) {
|
|
22928
|
+
let filtered = types;
|
|
22941
22929
|
if (opts.severity && opts.severity.length > 0) {
|
|
22942
22930
|
const sevSet = new Set(opts.severity);
|
|
22943
22931
|
filtered = filtered.filter((g) => sevSet.has(g.severity));
|
|
@@ -22948,7 +22936,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22948
22936
|
}
|
|
22949
22937
|
if (opts.actions && opts.actions.length > 0) {
|
|
22950
22938
|
const actionSet = new Set(opts.actions);
|
|
22951
|
-
filtered = filtered.filter((
|
|
22939
|
+
filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
|
|
22952
22940
|
}
|
|
22953
22941
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
22954
22942
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -22960,7 +22948,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22960
22948
|
}
|
|
22961
22949
|
if (opts.q) {
|
|
22962
22950
|
const q = opts.q.toLowerCase();
|
|
22963
|
-
filtered = filtered.filter((
|
|
22951
|
+
filtered = filtered.filter((t) => typeHaystack(t).includes(q));
|
|
22964
22952
|
}
|
|
22965
22953
|
return filtered;
|
|
22966
22954
|
}
|
|
@@ -22975,11 +22963,11 @@ function compareFindingGroupOrder(a, b) {
|
|
|
22975
22963
|
if (recencyDiff !== 0) return recencyDiff;
|
|
22976
22964
|
return a.id.localeCompare(b.id);
|
|
22977
22965
|
}
|
|
22978
|
-
function
|
|
22979
|
-
return [...
|
|
22966
|
+
function sortFindingTypes(types) {
|
|
22967
|
+
return [...types].sort(compareFindingGroupOrder);
|
|
22980
22968
|
}
|
|
22981
|
-
function computeFindingFacets(
|
|
22982
|
-
const forSeverity = applyFindingFilters(
|
|
22969
|
+
function computeFindingFacets(allTypes, opts) {
|
|
22970
|
+
const forSeverity = applyFindingFilters(allTypes, {
|
|
22983
22971
|
providers: opts.providers,
|
|
22984
22972
|
actions: opts.actions,
|
|
22985
22973
|
statuses: opts.statuses,
|
|
@@ -22990,7 +22978,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22990
22978
|
for (const g of forSeverity) {
|
|
22991
22979
|
severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
|
|
22992
22980
|
}
|
|
22993
|
-
const forProvider = applyFindingFilters(
|
|
22981
|
+
const forProvider = applyFindingFilters(allTypes, {
|
|
22994
22982
|
actions: opts.actions,
|
|
22995
22983
|
statuses: opts.statuses,
|
|
22996
22984
|
q: opts.q,
|
|
@@ -23001,7 +22989,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23001
22989
|
for (const g of forProvider) {
|
|
23002
22990
|
for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
|
|
23003
22991
|
}
|
|
23004
|
-
const forAction = applyFindingFilters(
|
|
22992
|
+
const forAction = applyFindingFilters(allTypes, {
|
|
23005
22993
|
providers: opts.providers,
|
|
23006
22994
|
statuses: opts.statuses,
|
|
23007
22995
|
q: opts.q,
|
|
@@ -23010,9 +22998,9 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23010
22998
|
});
|
|
23011
22999
|
const actionMap = /* @__PURE__ */ new Map();
|
|
23012
23000
|
for (const g of forAction) {
|
|
23013
|
-
for (const a of
|
|
23001
|
+
for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
23014
23002
|
}
|
|
23015
|
-
const forSubtype = applyFindingFilters(
|
|
23003
|
+
const forSubtype = applyFindingFilters(allTypes, {
|
|
23016
23004
|
providers: opts.providers,
|
|
23017
23005
|
actions: opts.actions,
|
|
23018
23006
|
statuses: opts.statuses,
|
|
@@ -23021,7 +23009,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23021
23009
|
});
|
|
23022
23010
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
23023
23011
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
23024
|
-
const forStatus = applyFindingFilters(
|
|
23012
|
+
const forStatus = applyFindingFilters(allTypes, {
|
|
23025
23013
|
providers: opts.providers,
|
|
23026
23014
|
actions: opts.actions,
|
|
23027
23015
|
q: opts.q,
|
|
@@ -23069,10 +23057,20 @@ function matchesDimension(row, opts, dimension) {
|
|
|
23069
23057
|
return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
|
|
23070
23058
|
case "tools":
|
|
23071
23059
|
return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
|
|
23060
|
+
// An EMPTY value is a real filter here, not an absent one. The location
|
|
23061
|
+
// list buckets a finding whose event recorded no repo — or no file — under
|
|
23062
|
+
// the empty string, and selecting that bucket has to narrow the panel to
|
|
23063
|
+
// exactly it. Only `undefined` means "no filter"; a caller that wants every
|
|
23064
|
+
// row omits the key, which every call site already does.
|
|
23065
|
+
//
|
|
23066
|
+
// Reading '' as unset is what this replaced, and it failed in the one place
|
|
23067
|
+
// it mattered: the no-repo/no-file bucket is often the largest in a real
|
|
23068
|
+
// store, and its panel dropped both predicates and returned the WHOLE scope
|
|
23069
|
+
// — a row reading 3 findings beside a panel listing every finding there is.
|
|
23072
23070
|
case "repo":
|
|
23073
|
-
return opts.repo === void 0 ||
|
|
23071
|
+
return opts.repo === void 0 || row.repo === opts.repo;
|
|
23074
23072
|
case "file":
|
|
23075
|
-
return opts.file === void 0 ||
|
|
23073
|
+
return opts.file === void 0 || row.file === opts.file;
|
|
23076
23074
|
case "q":
|
|
23077
23075
|
return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
|
|
23078
23076
|
}
|
|
@@ -23186,6 +23184,23 @@ function addToLocation(acc, row) {
|
|
|
23186
23184
|
acc.statuses.push(row.status);
|
|
23187
23185
|
acc.ruleIds.add(row.ruleId);
|
|
23188
23186
|
}
|
|
23187
|
+
function compareLocationOrder(a, b) {
|
|
23188
|
+
const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
|
|
23189
|
+
const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
|
|
23190
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
23191
|
+
if (a.latestDetectedAt !== b.latestDetectedAt) {
|
|
23192
|
+
return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
|
|
23193
|
+
}
|
|
23194
|
+
if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
|
|
23195
|
+
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
23196
|
+
return 0;
|
|
23197
|
+
}
|
|
23198
|
+
function encodeLocationId(repo, file2) {
|
|
23199
|
+
return `${encodePart(repo)}/${encodePart(file2)}`;
|
|
23200
|
+
}
|
|
23201
|
+
function encodePart(value) {
|
|
23202
|
+
return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
|
|
23203
|
+
}
|
|
23189
23204
|
|
|
23190
23205
|
// ../../packages/schema/src/zod/installed-pack.ts
|
|
23191
23206
|
var InstalledPack = external_exports.object({
|
|
@@ -23700,11 +23715,11 @@ var WorkspaceSettings = external_exports.object({
|
|
|
23700
23715
|
// covers the current payload and must be re-granted.
|
|
23701
23716
|
modelJudgeConsent: ModelJudgeConsent.optional(),
|
|
23702
23717
|
// Records that the user consented to the DEFERRED send — the outbox — along
|
|
23703
|
-
// with the payload shape and the endpoint they agreed to. Since payload
|
|
23704
|
-
// that covers
|
|
23705
|
-
// carry prompt/reply text in `content
|
|
23706
|
-
// Absent until granted, and a grant for a different endpoint
|
|
23707
|
-
// payload no longer counts.
|
|
23718
|
+
// with the payload shape and the endpoint they agreed to. Since payload v3
|
|
23719
|
+
// that covers the pre-attach backlog AND undelivered captures alike, and both
|
|
23720
|
+
// carry prompt/reply/tool-output text in `content`; the key name predates
|
|
23721
|
+
// both widenings. Absent until granted, and a grant for a different endpoint
|
|
23722
|
+
// or an older payload no longer counts.
|
|
23708
23723
|
historySyncConsent: HistorySyncConsent.optional()
|
|
23709
23724
|
});
|
|
23710
23725
|
function defaultWorkspaceSettings() {
|
|
@@ -23835,6 +23850,9 @@ var ManagedSettingKey = external_exports.enum([
|
|
|
23835
23850
|
"dataSharesInPlace",
|
|
23836
23851
|
"redactFallback"
|
|
23837
23852
|
]).meta({ id: "ManagedSettingKey" });
|
|
23853
|
+
function isManagedSettingKey(value) {
|
|
23854
|
+
return ManagedSettingKey.safeParse(value).success;
|
|
23855
|
+
}
|
|
23838
23856
|
var ManagedSettingsValues = external_exports.object({
|
|
23839
23857
|
runMode: external_exports.enum(["standalone", "attached"]).optional(),
|
|
23840
23858
|
controlPlane: external_exports.object({
|
|
@@ -23859,7 +23877,27 @@ var ManagedSettings = external_exports.object({
|
|
|
23859
23877
|
// Which of those the user may not change. A key here with no matching value
|
|
23860
23878
|
// freezes whatever the user last chose; a value with no lock is a DEFAULT
|
|
23861
23879
|
// the user may still override. The two are separable on purpose.
|
|
23862
|
-
|
|
23880
|
+
//
|
|
23881
|
+
// Parsed as NAMES rather than as the enum, and split below: a name this
|
|
23882
|
+
// build does not know is dropped from the locked set and reported, never a
|
|
23883
|
+
// reason to refuse the file. The same shape reaches an older build whenever
|
|
23884
|
+
// an administrator locks a key a newer build added, and refusing it there
|
|
23885
|
+
// ran that build entirely unmanaged — every pin and lock gone — on exactly
|
|
23886
|
+
// the fleets most likely to carry a version skew. A name outside the enum
|
|
23887
|
+
// is still never HONOURED: the lockable set stays explicit above.
|
|
23888
|
+
lockedFields: external_exports.array(external_exports.string()).default([])
|
|
23889
|
+
}).transform(({ lockedFields, ...rest }) => {
|
|
23890
|
+
const known = [];
|
|
23891
|
+
const unknown2 = [];
|
|
23892
|
+
for (const name of lockedFields) {
|
|
23893
|
+
if (isManagedSettingKey(name)) known.push(name);
|
|
23894
|
+
else unknown2.push(name);
|
|
23895
|
+
}
|
|
23896
|
+
return {
|
|
23897
|
+
...rest,
|
|
23898
|
+
lockedFields: known,
|
|
23899
|
+
...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
|
|
23900
|
+
};
|
|
23863
23901
|
}).meta({ id: "ManagedSettings" });
|
|
23864
23902
|
|
|
23865
23903
|
// ../../packages/schema/src/zod/project-files.ts
|
|
@@ -23983,7 +24021,11 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
23983
24021
|
timestamp: external_exports.iso.date(),
|
|
23984
24022
|
critical: external_exports.number().int().nonnegative(),
|
|
23985
24023
|
high: external_exports.number().int().nonnegative(),
|
|
23986
|
-
medium: external_exports.number().int().nonnegative()
|
|
24024
|
+
medium: external_exports.number().int().nonnegative(),
|
|
24025
|
+
// Optional and additive, so a producer written against the earlier
|
|
24026
|
+
// three-series contract keeps validating. A consumer plotting it resolves the
|
|
24027
|
+
// absent case itself — the chart point requires a number.
|
|
24028
|
+
low: external_exports.number().int().nonnegative().optional()
|
|
23987
24029
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
23988
24030
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
23989
24031
|
range: TimeRange,
|
|
@@ -24009,6 +24051,10 @@ var ResolvedFeedItem = external_exports.object({
|
|
|
24009
24051
|
findingKey: external_exports.string(),
|
|
24010
24052
|
ruleId: external_exports.string(),
|
|
24011
24053
|
severity: Severity,
|
|
24054
|
+
// Repository slug, and the file path RELATIVE to it. The pair is what
|
|
24055
|
+
// identifies the file: a bare path matches the same name in every repo.
|
|
24056
|
+
// Optional and additive; empty when the event carried no repo.
|
|
24057
|
+
repo: external_exports.string().optional(),
|
|
24012
24058
|
path: external_exports.string(),
|
|
24013
24059
|
// ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
|
|
24014
24060
|
// findings domain). The reader `.toISOString()`s the DB epoch-ms values.
|
|
@@ -25414,7 +25460,8 @@ var SqliteActivityRepository = class {
|
|
|
25414
25460
|
SELECT 1 FROM audit_events d
|
|
25415
25461
|
WHERE d.root_session_id = audit_events.id
|
|
25416
25462
|
AND (d.content LIKE ? ESCAPE '\\'
|
|
25417
|
-
OR json_extract(d.attributes, '$.detail')
|
|
25463
|
+
OR coalesce(json_extract(d.attributes, '$.detail'),
|
|
25464
|
+
json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
|
|
25418
25465
|
);
|
|
25419
25466
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
25420
25467
|
}
|
|
@@ -26752,23 +26799,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
26752
26799
|
)`;
|
|
26753
26800
|
|
|
26754
26801
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
26755
|
-
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
26756
|
-
var DEFAULT_LOCATIONS_LIMIT = 100;
|
|
26757
|
-
var LOCATION_RULE_IDS_CAP = 20;
|
|
26758
|
-
function compareLocationOrder(a, b) {
|
|
26759
|
-
return compareFindingGroupOrder(
|
|
26760
|
-
{
|
|
26761
|
-
severity: a.maxSeverity,
|
|
26762
|
-
latestDetectedAt: a.latestDetectedAt,
|
|
26763
|
-
id: ""
|
|
26764
|
-
},
|
|
26765
|
-
{
|
|
26766
|
-
severity: b.maxSeverity,
|
|
26767
|
-
latestDetectedAt: b.latestDetectedAt,
|
|
26768
|
-
id: ""
|
|
26769
|
-
}
|
|
26770
|
-
);
|
|
26771
|
-
}
|
|
26772
26802
|
var CONCAT_SEP = ",";
|
|
26773
26803
|
var TUPLE_SEP = "|";
|
|
26774
26804
|
function splitConcat(value) {
|
|
@@ -26820,13 +26850,48 @@ function decodeGroupCursor(cursor) {
|
|
|
26820
26850
|
return null;
|
|
26821
26851
|
}
|
|
26822
26852
|
function firstAfter(sorted, cursor) {
|
|
26823
|
-
const index = sorted.findIndex((
|
|
26853
|
+
const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
|
|
26824
26854
|
return index === -1 ? sorted.length : index;
|
|
26825
26855
|
}
|
|
26826
26856
|
function findDeepLinked(sorted, page, id) {
|
|
26827
|
-
if (page.some((
|
|
26828
|
-
return sorted.find((
|
|
26857
|
+
if (page.some((t) => t.id === id)) return void 0;
|
|
26858
|
+
return sorted.find((t) => t.id === id);
|
|
26829
26859
|
}
|
|
26860
|
+
function encodeLocationCursor(location) {
|
|
26861
|
+
const payload = {
|
|
26862
|
+
sev: location.maxSeverity,
|
|
26863
|
+
t: location.latestDetectedAt,
|
|
26864
|
+
r: location.repo,
|
|
26865
|
+
f: location.file
|
|
26866
|
+
};
|
|
26867
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
26868
|
+
}
|
|
26869
|
+
function decodeLocationCursor(cursor) {
|
|
26870
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
26871
|
+
if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.r === "string" && typeof parsed.f === "string") {
|
|
26872
|
+
return { maxSeverity: parsed.sev, latestDetectedAt: parsed.t, repo: parsed.r, file: parsed.f };
|
|
26873
|
+
}
|
|
26874
|
+
return null;
|
|
26875
|
+
}
|
|
26876
|
+
function firstLocationAfter(sorted, cursor) {
|
|
26877
|
+
const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
|
|
26878
|
+
return index === -1 ? sorted.length : index;
|
|
26879
|
+
}
|
|
26880
|
+
function findDeepLinkedLocation(sorted, page, id) {
|
|
26881
|
+
if (page.some((l) => l.id === id)) return void 0;
|
|
26882
|
+
return sorted.find((l) => l.id === id);
|
|
26883
|
+
}
|
|
26884
|
+
var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
26885
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
26886
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
26887
|
+
e.started_at AS occurred_at,
|
|
26888
|
+
e.source_tool AS source_tool,
|
|
26889
|
+
e.repo AS repo,
|
|
26890
|
+
e.file_path AS file,
|
|
26891
|
+
e.tool_name AS tool_name,
|
|
26892
|
+
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
26893
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
26894
|
+
${latestResolutionStatusSql("f")} AS latest_status`;
|
|
26830
26895
|
var DAY_MS3 = 864e5;
|
|
26831
26896
|
var SqliteFindingsRepository = class {
|
|
26832
26897
|
constructor(db) {
|
|
@@ -26947,30 +27012,26 @@ var SqliteFindingsRepository = class {
|
|
|
26947
27012
|
);
|
|
26948
27013
|
}
|
|
26949
27014
|
/**
|
|
26950
|
-
*
|
|
26951
|
-
*
|
|
26952
|
-
*
|
|
26953
|
-
*
|
|
26954
|
-
*
|
|
26955
|
-
*
|
|
26956
|
-
*
|
|
26957
|
-
*
|
|
26958
|
-
*
|
|
26959
|
-
*
|
|
26960
|
-
*
|
|
26961
|
-
*
|
|
27015
|
+
* Finding TYPES for the dashboard — one row per rule, scoped to the four
|
|
27016
|
+
* capture kinds (audit_events also holds structural/reconciler/scan rows this
|
|
27017
|
+
* list must never surface), with per-filter-excluded facets, the requested
|
|
27018
|
+
* filters applied, and sorted by severity then recency. Filtering and faceting
|
|
27019
|
+
* run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
|
|
27020
|
+
* full filtered set; `items` is the requested page (default 50), keyset-paged.
|
|
27021
|
+
* Under a `status` filter, `totals.findings` counts only findings whose
|
|
27022
|
+
* derived status was requested.
|
|
27023
|
+
*
|
|
27024
|
+
* ONE read, which materializes no findings: a single aggregate per rule_id,
|
|
27025
|
+
* folding EVERY finding into the numbers a type row and the filters need
|
|
27026
|
+
* (count, severity, category, providers, actions, statuses, latest, search
|
|
27027
|
+
* text). The findings OF a type come from listFindingInstances scoped to
|
|
27028
|
+
* `subtype`, so neither list bounds the other and no per-type cap exists.
|
|
26962
27029
|
*
|
|
26963
|
-
* Two reads, neither of which materializes a row per finding:
|
|
26964
|
-
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
26965
|
-
* the group and the filters need (count, providers, actions, statuses,
|
|
26966
|
-
* latest, search text);
|
|
26967
|
-
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
26968
|
-
* populate `instances` for the table's expanded rows.
|
|
26969
27030
|
* The aggregates carry raw DB values and are translated by the same
|
|
26970
|
-
* @akasecurity/schema mappers
|
|
26971
|
-
* rule is ever restated in SQL.
|
|
27031
|
+
* @akasecurity/schema mappers every other path uses, so no enum mapping or
|
|
27032
|
+
* status rule is ever restated in SQL.
|
|
26972
27033
|
*/
|
|
26973
|
-
|
|
27034
|
+
listFindingTypes(query) {
|
|
26974
27035
|
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
26975
27036
|
const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
|
|
26976
27037
|
const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
|
|
@@ -26983,12 +27044,7 @@ var SqliteFindingsRepository = class {
|
|
|
26983
27044
|
predicate,
|
|
26984
27045
|
params: sessionParams
|
|
26985
27046
|
});
|
|
26986
|
-
const
|
|
26987
|
-
sessionId: query.sessionId,
|
|
26988
|
-
from: query.from
|
|
26989
|
-
});
|
|
26990
|
-
const groupable = rows.map(toFlatFindingRow);
|
|
26991
|
-
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
27047
|
+
const allTypes = buildFindingTypes(aggregates);
|
|
26992
27048
|
const filterOpts = {
|
|
26993
27049
|
severity: query.severity,
|
|
26994
27050
|
providers: query.provider,
|
|
@@ -26997,30 +27053,25 @@ var SqliteFindingsRepository = class {
|
|
|
26997
27053
|
subtype: query.subtype,
|
|
26998
27054
|
q: query.q
|
|
26999
27055
|
};
|
|
27000
|
-
const facets = computeFindingFacets(
|
|
27001
|
-
const sorted =
|
|
27056
|
+
const facets = computeFindingFacets(allTypes, filterOpts);
|
|
27057
|
+
const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
|
|
27002
27058
|
const statusFilter = query.status ?? [];
|
|
27003
27059
|
const totals = {
|
|
27004
|
-
findings: sorted.reduce((acc,
|
|
27005
|
-
if (statusFilter.length === 0) return acc +
|
|
27006
|
-
const agg = aggregates.get(
|
|
27007
|
-
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ??
|
|
27060
|
+
findings: sorted.reduce((acc, t) => {
|
|
27061
|
+
if (statusFilter.length === 0) return acc + t.instanceCount;
|
|
27062
|
+
const agg = aggregates.get(t.id);
|
|
27063
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
|
|
27008
27064
|
}, 0),
|
|
27009
|
-
|
|
27065
|
+
types: sorted.length
|
|
27010
27066
|
};
|
|
27011
|
-
const limit = query.limit ??
|
|
27067
|
+
const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
|
|
27012
27068
|
const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
|
|
27013
27069
|
const start = cursor === null ? 0 : firstAfter(sorted, cursor);
|
|
27014
27070
|
const page = sorted.slice(start, start + limit);
|
|
27015
27071
|
const lastOnPage = page.at(-1);
|
|
27016
27072
|
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
|
|
27017
27073
|
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
|
|
27018
|
-
const
|
|
27019
|
-
const narrow = (g) => statusSet ? {
|
|
27020
|
-
...g,
|
|
27021
|
-
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
27022
|
-
} : g;
|
|
27023
|
-
const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
|
|
27074
|
+
const items = [...page, ...deepLinked ? [deepLinked] : []];
|
|
27024
27075
|
return Promise.resolve({
|
|
27025
27076
|
totals,
|
|
27026
27077
|
facets,
|
|
@@ -27031,7 +27082,7 @@ var SqliteFindingsRepository = class {
|
|
|
27031
27082
|
}
|
|
27032
27083
|
/**
|
|
27033
27084
|
* One row per rule_id, folding EVERY instance of the group into the values
|
|
27034
|
-
*
|
|
27085
|
+
* buildFindingTypes cannot recover from an aggregate. Bounded by the number of
|
|
27035
27086
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
27036
27087
|
*
|
|
27037
27088
|
* A single scan, folded in two levels: the inner SELECT groups by
|
|
@@ -27125,13 +27176,25 @@ var SqliteFindingsRepository = class {
|
|
|
27125
27176
|
});
|
|
27126
27177
|
}
|
|
27127
27178
|
/**
|
|
27128
|
-
* The same findings folded by
|
|
27179
|
+
* The same findings folded by WHERE they live — one row per (repo, file) pair.
|
|
27129
27180
|
*
|
|
27130
27181
|
* The grouping keys come from the capturing event's attributes, which is what
|
|
27131
|
-
* the local store relates a finding to
|
|
27132
|
-
*
|
|
27133
|
-
* empty-string bucket, which
|
|
27134
|
-
*
|
|
27182
|
+
* the local store relates a finding to; there is no finding↔asset row to group
|
|
27183
|
+
* by instead. A repo or file the event did not record folds into the
|
|
27184
|
+
* empty-string bucket, which is a real location like any other: it is listed,
|
|
27185
|
+
* it is selectable, and its `?loc=` token is as good as any other row's.
|
|
27186
|
+
*
|
|
27187
|
+
* ONE flat list rather than repos nesting files. A rollup can only be paged by
|
|
27188
|
+
* repo, which leaves the file list inside it unbounded — the shape the by-type
|
|
27189
|
+
* list was rebuilt to remove — and two-level pagination inside an
|
|
27190
|
+
* expand/collapse table is what pushed that view to master/detail in the first
|
|
27191
|
+
* place.
|
|
27192
|
+
*
|
|
27193
|
+
* Every filter narrows the FINDINGS and the locations fall out of what
|
|
27194
|
+
* survives, so each row's `instanceCount` is exactly what listFindingInstances
|
|
27195
|
+
* reports for the same filters scoped to that pair. The view depends on it:
|
|
27196
|
+
* one toolbar sits over both panels precisely because a location owns none of
|
|
27197
|
+
* its fields.
|
|
27135
27198
|
*/
|
|
27136
27199
|
listFindingLocations(query) {
|
|
27137
27200
|
const opts = {
|
|
@@ -27143,13 +27206,16 @@ var SqliteFindingsRepository = class {
|
|
|
27143
27206
|
tools: query.tool,
|
|
27144
27207
|
q: query.q
|
|
27145
27208
|
};
|
|
27146
|
-
const limit = query.limit ??
|
|
27209
|
+
const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
|
|
27210
|
+
const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
|
|
27147
27211
|
const byRepo = /* @__PURE__ */ new Map();
|
|
27212
|
+
const accumulator = createInstanceFacetAccumulator(opts);
|
|
27148
27213
|
let total = 0;
|
|
27149
27214
|
for (const row of this.scanFindingRows({
|
|
27150
27215
|
sessionId: query.sessionId,
|
|
27151
27216
|
from: query.from
|
|
27152
27217
|
})) {
|
|
27218
|
+
accumulator.add(row);
|
|
27153
27219
|
if (!matchesInstanceFilters(row, opts)) continue;
|
|
27154
27220
|
total += 1;
|
|
27155
27221
|
let files = byRepo.get(row.repo);
|
|
@@ -27164,103 +27230,35 @@ var SqliteFindingsRepository = class {
|
|
|
27164
27230
|
}
|
|
27165
27231
|
addToLocation(acc, row);
|
|
27166
27232
|
}
|
|
27167
|
-
|
|
27168
|
-
const
|
|
27169
|
-
|
|
27170
|
-
|
|
27171
|
-
|
|
27172
|
-
|
|
27173
|
-
|
|
27174
|
-
|
|
27175
|
-
|
|
27176
|
-
|
|
27177
|
-
|
|
27178
|
-
|
|
27179
|
-
|
|
27180
|
-
|
|
27181
|
-
|
|
27182
|
-
|
|
27183
|
-
|
|
27184
|
-
|
|
27185
|
-
|
|
27186
|
-
|
|
27187
|
-
|
|
27188
|
-
|
|
27189
|
-
);
|
|
27190
|
-
const statuses = fileRows.map((f) => f.status);
|
|
27191
|
-
const folded = foldGroupStatus(statuses);
|
|
27192
|
-
return {
|
|
27193
|
-
repo,
|
|
27194
|
-
instanceCount: rollup.instanceCount,
|
|
27195
|
-
maxSeverity: rollup.maxSeverity,
|
|
27196
|
-
latestDetectedAt: rollup.latestDetectedAt,
|
|
27197
|
-
...folded === void 0 ? {} : { status: folded },
|
|
27198
|
-
files: fileRows
|
|
27199
|
-
};
|
|
27200
|
-
});
|
|
27201
|
-
repos.sort(compareLocationOrder);
|
|
27233
|
+
const sorted = [];
|
|
27234
|
+
for (const [repo, files] of byRepo) {
|
|
27235
|
+
for (const [file2, acc] of files) {
|
|
27236
|
+
const status = foldGroupStatus(acc.statuses);
|
|
27237
|
+
sorted.push({
|
|
27238
|
+
id: encodeLocationId(repo, file2),
|
|
27239
|
+
repo,
|
|
27240
|
+
file: file2,
|
|
27241
|
+
instanceCount: acc.instanceCount,
|
|
27242
|
+
maxSeverity: acc.maxSeverity,
|
|
27243
|
+
latestDetectedAt: acc.latestDetectedAt,
|
|
27244
|
+
...status === void 0 ? {} : { status },
|
|
27245
|
+
ruleIds: [...acc.ruleIds]
|
|
27246
|
+
});
|
|
27247
|
+
}
|
|
27248
|
+
}
|
|
27249
|
+
sorted.sort(compareLocationOrder);
|
|
27250
|
+
const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
|
|
27251
|
+
const page = sorted.slice(start, start + limit);
|
|
27252
|
+
const lastOnPage = page.at(-1);
|
|
27253
|
+
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
|
|
27254
|
+
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
|
|
27202
27255
|
return Promise.resolve({
|
|
27203
|
-
totals: { findings: total,
|
|
27204
|
-
|
|
27205
|
-
|
|
27256
|
+
totals: { findings: total, locations: sorted.length },
|
|
27257
|
+
facets: accumulator.facets(),
|
|
27258
|
+
items: [...page, ...deepLinked ? [deepLinked] : []],
|
|
27259
|
+
nextCursor
|
|
27206
27260
|
});
|
|
27207
27261
|
}
|
|
27208
|
-
/**
|
|
27209
|
-
* Each group's newest instances, for the table's expanded rows.
|
|
27210
|
-
*
|
|
27211
|
-
* ONE index-ordered scan with early termination, and the shape is the point.
|
|
27212
|
-
* The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
|
|
27213
|
-
* started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
|
|
27214
|
-
* through a temp B-tree to keep a bounded preview of each group, and then
|
|
27215
|
-
* sorts the survivors again for the page order. Both sorts grow with the
|
|
27216
|
-
* store while the answer does not.
|
|
27217
|
-
*
|
|
27218
|
-
* Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
|
|
27219
|
-
* (or the session or window index the scope names — see `findingScanSql`),
|
|
27220
|
-
* which is already the order the page wants, and keeps rows per rule until
|
|
27221
|
-
* each rule has as many as it can show. The aggregate the caller already holds
|
|
27222
|
-
* says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
|
|
27223
|
-
* per rule, summed, is the number of rows this scan has to find, and it stops
|
|
27224
|
-
* on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
|
|
27225
|
-
* (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
|
|
27226
|
-
* store with many firing rules widens it. The bound that DOES hold
|
|
27227
|
-
* unconditionally is the sorted form's floor: this scan visits at most as
|
|
27228
|
-
* many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
|
|
27229
|
-
* sorted, and stops the moment every rule has its cap, where the sorted form
|
|
27230
|
-
* sorts the whole scope regardless. The true worst case — the rarest rule's
|
|
27231
|
-
* wanted instances sitting at the tail of the scope — is one pass over
|
|
27232
|
-
* everything in scope with a block sort of the id tie-break only, never a
|
|
27233
|
-
* sort of the scope, which is still that floor.
|
|
27234
|
-
*
|
|
27235
|
-
* A row whose rule the aggregate did not see is skipped: the two statements
|
|
27236
|
-
* run without a shared snapshot, so a capture landing between them can add a
|
|
27237
|
-
* rule here that has no counts there, and the counts are what the group is
|
|
27238
|
-
* built from.
|
|
27239
|
-
*/
|
|
27240
|
-
previewRows(aggregates, scope) {
|
|
27241
|
-
const wanted = /* @__PURE__ */ new Map();
|
|
27242
|
-
let remaining = 0;
|
|
27243
|
-
for (const [ruleId, agg] of aggregates) {
|
|
27244
|
-
const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
|
|
27245
|
-
wanted.set(ruleId, n);
|
|
27246
|
-
remaining += n;
|
|
27247
|
-
}
|
|
27248
|
-
const rows = [];
|
|
27249
|
-
if (remaining === 0) return rows;
|
|
27250
|
-
const { sql, params } = this.findingScanSql(scope);
|
|
27251
|
-
const taken = /* @__PURE__ */ new Map();
|
|
27252
|
-
for (const r of iterateRows(this.db.prepare(sql), params)) {
|
|
27253
|
-
const want = wanted.get(r.rule_id);
|
|
27254
|
-
if (want === void 0) continue;
|
|
27255
|
-
const have = taken.get(r.rule_id) ?? 0;
|
|
27256
|
-
if (have >= want) continue;
|
|
27257
|
-
taken.set(r.rule_id, have + 1);
|
|
27258
|
-
rows.push(r);
|
|
27259
|
-
remaining -= 1;
|
|
27260
|
-
if (remaining === 0) break;
|
|
27261
|
-
}
|
|
27262
|
-
return rows;
|
|
27263
|
-
}
|
|
27264
27262
|
/**
|
|
27265
27263
|
* Every finding in scope as a FlatFindingRow, newest first, streamed.
|
|
27266
27264
|
*
|
|
@@ -27287,6 +27285,33 @@ var SqliteFindingsRepository = class {
|
|
|
27287
27285
|
yield toFlatFindingRow(r);
|
|
27288
27286
|
}
|
|
27289
27287
|
}
|
|
27288
|
+
/**
|
|
27289
|
+
* One finding by its own id, or null when no such row exists.
|
|
27290
|
+
*
|
|
27291
|
+
* A primary-key seek on `inspection_findings`, so its cost does not grow with
|
|
27292
|
+
* the store — and, unlike anything derived from a list page, it resolves a
|
|
27293
|
+
* finding of ANY age. That is what the Findings page's one-shot `?finding=`
|
|
27294
|
+
* deep link needs: the id it carries may name a finding thousands of rows
|
|
27295
|
+
* older than anything a first page holds.
|
|
27296
|
+
*
|
|
27297
|
+
* Deliberately UNFILTERED — no capture-kind, session or time predicate. It
|
|
27298
|
+
* RESOLVES an id; whether that row would survive the list's current filters is
|
|
27299
|
+
* a different question, and hiding the target because a filter excludes it is
|
|
27300
|
+
* worse than showing it.
|
|
27301
|
+
*
|
|
27302
|
+
* `groupId` on the result IS the rule id, so this one read answers both "which
|
|
27303
|
+
* type should the list select?" and "what does the drawer show?".
|
|
27304
|
+
*/
|
|
27305
|
+
findingInstance(id) {
|
|
27306
|
+
const row = this.db.prepare(
|
|
27307
|
+
`SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27308
|
+
FROM inspection_findings f
|
|
27309
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
27310
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27311
|
+
WHERE f.id = ?`
|
|
27312
|
+
).get(id);
|
|
27313
|
+
return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
|
|
27314
|
+
}
|
|
27290
27315
|
/**
|
|
27291
27316
|
* The one statement both instance-level scans run: every finding in scope,
|
|
27292
27317
|
* joined to its event and definition, newest first.
|
|
@@ -27320,17 +27345,7 @@ var SqliteFindingsRepository = class {
|
|
|
27320
27345
|
conditions.push("e.started_at >= ?");
|
|
27321
27346
|
params.push(isoToEpochMillis(scope.from));
|
|
27322
27347
|
}
|
|
27323
|
-
const sql = `SELECT
|
|
27324
|
-
d.severity AS severity, f.masked_match AS masked_match,
|
|
27325
|
-
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
27326
|
-
e.started_at AS occurred_at,
|
|
27327
|
-
e.source_tool AS source_tool,
|
|
27328
|
-
e.repo AS repo,
|
|
27329
|
-
e.file_path AS file,
|
|
27330
|
-
e.tool_name AS tool_name,
|
|
27331
|
-
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
27332
|
-
e.event_type AS kind, f.finding_key AS finding_key,
|
|
27333
|
-
${latestResolutionStatusSql("f")} AS latest_status
|
|
27348
|
+
const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27334
27349
|
FROM audit_events e
|
|
27335
27350
|
CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
|
|
27336
27351
|
CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -27344,6 +27359,26 @@ var SqliteFindingsRepository = class {
|
|
|
27344
27359
|
group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
27345
27360
|
const rows = this.db.prepare(
|
|
27346
27361
|
`SELECT rule_id,
|
|
27362
|
+
-- BARE columns beside max(latest_at), which is deliberate and
|
|
27363
|
+
-- is SQLite's documented behaviour: with a single min()/max()
|
|
27364
|
+
-- in an aggregate query, every bare column takes its value from
|
|
27365
|
+
-- the row that produced the extremum. So these are the severity
|
|
27366
|
+
-- and category of the definition whose finding is NEWEST, which
|
|
27367
|
+
-- is what the row-based build they replaced read off its first
|
|
27368
|
+
-- (newest-first) row.
|
|
27369
|
+
--
|
|
27370
|
+
-- min() is WRONG here and was the defect: inspection_definitions
|
|
27371
|
+
-- holds one row per rule VERSION (see its writer \u2014 a version bump
|
|
27372
|
+
-- mints a new row), so a rule whose severity moved between
|
|
27373
|
+
-- versions has several, and min() picks the ALPHABETICALLY
|
|
27374
|
+
-- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
|
|
27375
|
+
-- That is arbitrary in direction, and it feeds the badge, the
|
|
27376
|
+
-- filter, the facet counts and the primary sort key.
|
|
27377
|
+
--
|
|
27378
|
+
-- Adding a second min()/max() aggregate here would make these
|
|
27379
|
+
-- bare columns ambiguous again; keep max(latest_at) the only one.
|
|
27380
|
+
severity,
|
|
27381
|
+
category,
|
|
27347
27382
|
sum(tuple_count) AS instance_count,
|
|
27348
27383
|
max(latest_at) AS latest_at,
|
|
27349
27384
|
group_concat(source_tools) AS source_tools,
|
|
@@ -27354,6 +27389,14 @@ var SqliteFindingsRepository = class {
|
|
|
27354
27389
|
group_concat(tool_names) AS tool_names
|
|
27355
27390
|
FROM (
|
|
27356
27391
|
SELECT d.rule_id AS rule_id,
|
|
27392
|
+
-- Severity and category are columns of the DEFINITION, and
|
|
27393
|
+
-- a rule can have SEVERAL definitions (one per version), so
|
|
27394
|
+
-- these are grouped on below and resolved to the newest
|
|
27395
|
+
-- firing version by the outer query's bare-column select.
|
|
27396
|
+
-- They ride the aggregate because the type build has no rows
|
|
27397
|
+
-- to read them off \u2014 see buildFindingTypes.
|
|
27398
|
+
d.severity AS severity,
|
|
27399
|
+
d.category AS category,
|
|
27357
27400
|
e.event_type || '${TUPLE_SEP}' ||
|
|
27358
27401
|
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
27359
27402
|
coalesce(latest.status, '') AS status_tuple,
|
|
@@ -27368,7 +27411,7 @@ var SqliteFindingsRepository = class {
|
|
|
27368
27411
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
27369
27412
|
ON latest.finding_key = f.finding_key
|
|
27370
27413
|
${scope.predicate}
|
|
27371
|
-
GROUP BY d.rule_id, status_tuple
|
|
27414
|
+
GROUP BY d.rule_id, d.severity, d.category, status_tuple
|
|
27372
27415
|
)
|
|
27373
27416
|
GROUP BY rule_id`
|
|
27374
27417
|
).all(scope.params);
|
|
@@ -27377,6 +27420,8 @@ var SqliteFindingsRepository = class {
|
|
|
27377
27420
|
r.rule_id,
|
|
27378
27421
|
{
|
|
27379
27422
|
instanceCount: r.instance_count,
|
|
27423
|
+
severity: r.severity,
|
|
27424
|
+
category: r.category,
|
|
27380
27425
|
sourceTools: splitConcat(r.source_tools),
|
|
27381
27426
|
actionsTaken: splitConcat(r.actions_taken),
|
|
27382
27427
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
@@ -27393,7 +27438,7 @@ var SqliteFindingsRepository = class {
|
|
|
27393
27438
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
27394
27439
|
// Free text only — joined and substring-matched, so group_concat's
|
|
27395
27440
|
// commas need no unpicking (a repo/path containing one still matches).
|
|
27396
|
-
// Left undefined (not '') when unfetched, so
|
|
27441
|
+
// Left undefined (not '') when unfetched, so buildFindingTypes can
|
|
27397
27442
|
// tell "no q this request" from "a group with no repo/file at all"
|
|
27398
27443
|
// and skip priming a haystack nothing will read.
|
|
27399
27444
|
...withSearchText ? {
|
|
@@ -27545,6 +27590,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27545
27590
|
this.markOwedStmt = db.prepare(
|
|
27546
27591
|
`UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
|
|
27547
27592
|
);
|
|
27593
|
+
this.markCaptureBacklogOwedStmt = db.prepare(
|
|
27594
|
+
`UPDATE audit_events SET outbox_owed = 1
|
|
27595
|
+
WHERE synced_at IS NULL
|
|
27596
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27597
|
+
AND started_at < :before`
|
|
27598
|
+
);
|
|
27548
27599
|
this.stampStmt = db.prepare(
|
|
27549
27600
|
`UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
|
|
27550
27601
|
);
|
|
@@ -27593,7 +27644,9 @@ var SqliteHistorySyncRepository = class {
|
|
|
27593
27644
|
);
|
|
27594
27645
|
this.disownCapturesStmt = db.prepare(
|
|
27595
27646
|
`UPDATE audit_events SET outbox_owed = NULL
|
|
27596
|
-
WHERE outbox_owed IS NOT NULL
|
|
27647
|
+
WHERE outbox_owed IS NOT NULL
|
|
27648
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27649
|
+
AND started_at < :attachedAt`
|
|
27597
27650
|
);
|
|
27598
27651
|
this.rearmStmt = db.prepare(
|
|
27599
27652
|
`UPDATE audit_events SET synced_at = NULL
|
|
@@ -27669,6 +27722,7 @@ var SqliteHistorySyncRepository = class {
|
|
|
27669
27722
|
freezeBoundaryStmt;
|
|
27670
27723
|
captureRowsStmt;
|
|
27671
27724
|
markOwedStmt;
|
|
27725
|
+
markCaptureBacklogOwedStmt;
|
|
27672
27726
|
captureSkipCountStmt;
|
|
27673
27727
|
disownCapturesStmt;
|
|
27674
27728
|
partitionStmt;
|
|
@@ -27732,6 +27786,23 @@ var SqliteHistorySyncRepository = class {
|
|
|
27732
27786
|
markCaptureOwed(id) {
|
|
27733
27787
|
this.markOwedStmt.run({ id });
|
|
27734
27788
|
}
|
|
27789
|
+
/**
|
|
27790
|
+
* Mark every capture already on disk as owed, as of `before`.
|
|
27791
|
+
*
|
|
27792
|
+
* The consent-time backfill, called once from `aka attach` when a human
|
|
27793
|
+
* grants existing-history consent — never from an ongoing drain pass, and
|
|
27794
|
+
* never inferred from a boundary that could later move. `before` is the
|
|
27795
|
+
* caller's own "now" at the moment consent was granted, so what this marks
|
|
27796
|
+
* is exactly the backlog the consent prompt already counted, not whatever a
|
|
27797
|
+
* later re-attach or key rotation might widen it to.
|
|
27798
|
+
*
|
|
27799
|
+
* Returns how many rows matched, for the caller to log or test against. Not a
|
|
27800
|
+
* count of NEWLY marked rows — a row still unsynced from an earlier call
|
|
27801
|
+
* matches again and is counted again, the same as `UPDATE`'s own `changes`.
|
|
27802
|
+
*/
|
|
27803
|
+
markCaptureBacklogOwed(before) {
|
|
27804
|
+
return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
|
|
27805
|
+
}
|
|
27735
27806
|
/** Record delivery. Called only AFTER the far side has accepted the rows. */
|
|
27736
27807
|
markSynced(ids, atMs) {
|
|
27737
27808
|
this.stampAll(ids, atMs);
|
|
@@ -27848,15 +27919,42 @@ var SqliteHistorySyncRepository = class {
|
|
|
27848
27919
|
*
|
|
27849
27920
|
* Delivery is a fact about ONE recipient: rows sent to the deployment a
|
|
27850
27921
|
* machine has just left are undelivered as far as the new one is concerned.
|
|
27851
|
-
* All
|
|
27852
|
-
* attributed to the wrong deployment,
|
|
27922
|
+
* All four in one transaction, so a crash between them cannot leave stamps
|
|
27923
|
+
* attributed to the wrong deployment, a boundary that belongs to another, or
|
|
27924
|
+
* a disown with no re-mark to follow it.
|
|
27853
27925
|
*
|
|
27854
27926
|
* The boundary is written HERE and only here, which is what freezes it: a
|
|
27855
27927
|
* re-attach to the SAME deployment (a key rotation) leaves the fingerprint
|
|
27856
27928
|
* unchanged, so this never runs and the backlog does not widen back over rows
|
|
27857
27929
|
* the live path has since delivered.
|
|
27930
|
+
*
|
|
27931
|
+
* `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
|
|
27932
|
+
* granted existing-history consent for the deployment this call is arming —
|
|
27933
|
+
* a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
|
|
27934
|
+
* instant, `backlogBefore` is the ATTACH instant, and the two can be far
|
|
27935
|
+
* apart. Passed only when that grant is valid, since this method has no way
|
|
27936
|
+
* to check consent itself and must not mark a row owed for a machine that
|
|
27937
|
+
* never agreed to it. Applied AFTER the disown above, in the SAME
|
|
27938
|
+
* transaction: what the disown clears is every marker below `backlogBefore`,
|
|
27939
|
+
* which includes this deployment's OWN pre-attach rows — `aka attach` calls
|
|
27940
|
+
* `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
|
|
27941
|
+
* on the cleared side of that bound — and the re-mark in the same
|
|
27942
|
+
* transaction is what puts those rows back. A crash between the two cannot
|
|
27943
|
+
* strand the ledger disowned with nothing re-marked — the transaction either
|
|
27944
|
+
* lands whole or not at all, and a fingerprint mismatch that has not yet
|
|
27945
|
+
* committed re-enters this method on the very next pass. Omit it (the
|
|
27946
|
+
* structural-only tests do) to exercise the disown in isolation.
|
|
27947
|
+
*
|
|
27948
|
+
* The disown is bounded by `backlogBefore`, which is what keeps it from
|
|
27949
|
+
* touching a marker the NEW deployment's OWN live path has already set: B's
|
|
27950
|
+
* live path can mark a capture owed from the moment `aka attach` writes the
|
|
27951
|
+
* descriptor, before the drain's first pass ever reaches this method, and
|
|
27952
|
+
* such a row sits at or after the bound rather than below it. What keeps the
|
|
27953
|
+
* disown from eating THIS SAME CALL's own re-mark is the order, not the
|
|
27954
|
+
* bound — disown runs first, re-mark second, both inside the one
|
|
27955
|
+
* transaction above.
|
|
27858
27956
|
*/
|
|
27859
|
-
rearmFor(fingerprint, backlogBefore) {
|
|
27957
|
+
rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
|
|
27860
27958
|
this.ensureRowStmt.run();
|
|
27861
27959
|
withTransaction(
|
|
27862
27960
|
this.db,
|
|
@@ -27864,7 +27962,10 @@ var SqliteHistorySyncRepository = class {
|
|
|
27864
27962
|
const previous = getRow(this.fingerprintStmt)?.fingerprint;
|
|
27865
27963
|
this.rearmStmt.run();
|
|
27866
27964
|
if (previous !== null && previous !== void 0 && previous !== fingerprint) {
|
|
27867
|
-
this.disownCapturesStmt.run();
|
|
27965
|
+
this.disownCapturesStmt.run({ attachedAt: backlogBefore });
|
|
27966
|
+
}
|
|
27967
|
+
if (backfillCapturesBefore !== void 0) {
|
|
27968
|
+
this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
|
|
27868
27969
|
}
|
|
27869
27970
|
this.setFingerprintStmt.run({ fingerprint, backlogBefore });
|
|
27870
27971
|
},
|
|
@@ -28137,7 +28238,8 @@ function managedSettingsPaths(platform2 = process.platform) {
|
|
|
28137
28238
|
}
|
|
28138
28239
|
return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
|
|
28139
28240
|
}
|
|
28140
|
-
|
|
28241
|
+
var testOnlyManagedPaths = null;
|
|
28242
|
+
function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
|
|
28141
28243
|
for (const path of paths) {
|
|
28142
28244
|
let text;
|
|
28143
28245
|
try {
|
|
@@ -30617,7 +30719,7 @@ function toUtcDateString(ms) {
|
|
|
30617
30719
|
return new Date(ms).toISOString().slice(0, 10);
|
|
30618
30720
|
}
|
|
30619
30721
|
function isTimeseriesSeverity(s) {
|
|
30620
|
-
return s === "critical" || s === "high" || s === "medium";
|
|
30722
|
+
return s === "critical" || s === "high" || s === "medium" || s === "low";
|
|
30621
30723
|
}
|
|
30622
30724
|
var SqliteSecurityRepository = class {
|
|
30623
30725
|
constructor(db, now = () => Date.now()) {
|
|
@@ -30746,12 +30848,16 @@ var SqliteSecurityRepository = class {
|
|
|
30746
30848
|
const now = this.now();
|
|
30747
30849
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
30748
30850
|
const rows = this.findingsInRange(windowStart, now);
|
|
30749
|
-
const points = Array.from(
|
|
30750
|
-
|
|
30751
|
-
|
|
30752
|
-
|
|
30753
|
-
|
|
30754
|
-
|
|
30851
|
+
const points = Array.from(
|
|
30852
|
+
{ length: numBuckets },
|
|
30853
|
+
(_, i) => ({
|
|
30854
|
+
timestamp: toUtcDateString(windowStart + i * bucketMs),
|
|
30855
|
+
critical: 0,
|
|
30856
|
+
high: 0,
|
|
30857
|
+
medium: 0,
|
|
30858
|
+
low: 0
|
|
30859
|
+
})
|
|
30860
|
+
);
|
|
30755
30861
|
for (const r of rows) {
|
|
30756
30862
|
const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
|
|
30757
30863
|
const bucket = points[idx];
|
|
@@ -30969,6 +31075,7 @@ var SqliteSecurityRepository = class {
|
|
|
30969
31075
|
`SELECT f.finding_key AS finding_key,
|
|
30970
31076
|
d.rule_id AS rule_id,
|
|
30971
31077
|
d.severity AS severity,
|
|
31078
|
+
e.repo AS repo,
|
|
30972
31079
|
e.file_path AS path,
|
|
30973
31080
|
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
30974
31081
|
latest.resolved_at AS latest_resolved_at
|
|
@@ -30988,6 +31095,7 @@ var SqliteSecurityRepository = class {
|
|
|
30988
31095
|
const items = rows.map((r) => ({
|
|
30989
31096
|
findingKey: r.finding_key,
|
|
30990
31097
|
ruleId: r.rule_id,
|
|
31098
|
+
repo: r.repo ?? "",
|
|
30991
31099
|
severity: r.severity,
|
|
30992
31100
|
path: r.path ?? "",
|
|
30993
31101
|
resolvedAt: new Date(r.latest_resolved_at).toISOString(),
|
|
@@ -30997,13 +31105,66 @@ var SqliteSecurityRepository = class {
|
|
|
30997
31105
|
}));
|
|
30998
31106
|
return Promise.resolve({ items });
|
|
30999
31107
|
}
|
|
31108
|
+
/**
|
|
31109
|
+
* Per-rule tallies of the findings that are still OPEN, whole-store.
|
|
31110
|
+
*
|
|
31111
|
+
* Scoped by status rather than by time, because the card this feeds is a to-do
|
|
31112
|
+
* list: a secret committed three weeks ago and never rotated is still the most
|
|
31113
|
+
* important thing to fix, and any window hides it. It carried a "newest N
|
|
31114
|
+
* findings" cap and then a range; the first meant a different span on every
|
|
31115
|
+
* machine, and the second reported "no recommendations" over live exposure.
|
|
31116
|
+
*
|
|
31117
|
+
* `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
|
|
31118
|
+
* so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
|
|
31119
|
+
* is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
|
|
31120
|
+
* dismissal is a judgement, not a remediation) and drops untracked legacy rows.
|
|
31121
|
+
* The two answer different questions and only this one has to match a link.
|
|
31122
|
+
*
|
|
31123
|
+
* Aggregated in SQL: the result is O(distinct rule × category × severity), so a
|
|
31124
|
+
* whole-store scope costs a grouped scan rather than a row per finding.
|
|
31125
|
+
*/
|
|
31126
|
+
recommendationInputs() {
|
|
31127
|
+
const rows = allRows(
|
|
31128
|
+
this.db.prepare(
|
|
31129
|
+
`SELECT d.rule_id AS rule_id,
|
|
31130
|
+
d.category AS category,
|
|
31131
|
+
d.severity AS severity,
|
|
31132
|
+
COUNT(*) AS count
|
|
31133
|
+
FROM inspection_findings f
|
|
31134
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31135
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
31136
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
31137
|
+
ON latest.finding_key = f.finding_key
|
|
31138
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
31139
|
+
AND e.event_type = 'code_change'
|
|
31140
|
+
AND (
|
|
31141
|
+
f.finding_key IS NULL
|
|
31142
|
+
OR latest.status IS NULL
|
|
31143
|
+
OR latest.status NOT IN ('resolved', 'dismissed')
|
|
31144
|
+
)
|
|
31145
|
+
GROUP BY d.rule_id, d.category, d.severity`
|
|
31146
|
+
)
|
|
31147
|
+
);
|
|
31148
|
+
return Promise.resolve(
|
|
31149
|
+
rows.map((r) => ({
|
|
31150
|
+
ruleId: r.rule_id,
|
|
31151
|
+
category: r.category,
|
|
31152
|
+
severity: r.severity,
|
|
31153
|
+
count: r.count
|
|
31154
|
+
}))
|
|
31155
|
+
);
|
|
31156
|
+
}
|
|
31000
31157
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
31001
31158
|
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
31002
31159
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
31003
31160
|
findingsInRange(fromMs, toMs) {
|
|
31004
31161
|
const rows = allRows(
|
|
31005
31162
|
this.db.prepare(
|
|
31006
|
-
`
|
|
31163
|
+
// `rule_id`/`category` cost nothing extra: inspection_definitions is already
|
|
31164
|
+
// joined for `severity`, so they are two more columns off a row this read
|
|
31165
|
+
// already fetches. They feed the recommended-actions rollup.
|
|
31166
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
|
|
31167
|
+
d.rule_id AS rule_id, d.category AS category
|
|
31007
31168
|
FROM inspection_findings f
|
|
31008
31169
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31009
31170
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -31016,7 +31177,9 @@ var SqliteSecurityRepository = class {
|
|
|
31016
31177
|
return rows.map((r) => ({
|
|
31017
31178
|
occurredAt: r.occurred_at,
|
|
31018
31179
|
severity: r.severity,
|
|
31019
|
-
actionTaken: r.action_taken
|
|
31180
|
+
actionTaken: r.action_taken,
|
|
31181
|
+
ruleId: r.rule_id,
|
|
31182
|
+
category: r.category
|
|
31020
31183
|
}));
|
|
31021
31184
|
}
|
|
31022
31185
|
};
|
|
@@ -32186,23 +32349,30 @@ function openLocalDatabase(dir) {
|
|
|
32186
32349
|
};
|
|
32187
32350
|
}
|
|
32188
32351
|
|
|
32189
|
-
// ../../packages/persistence/src/
|
|
32352
|
+
// ../../packages/persistence/src/egress-wire.ts
|
|
32190
32353
|
import { createHash as createHash3 } from "crypto";
|
|
32191
32354
|
|
|
32355
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
32356
|
+
import { createHash as createHash4 } from "crypto";
|
|
32357
|
+
|
|
32192
32358
|
// ../../packages/persistence/src/fingerprint.ts
|
|
32193
32359
|
import { createHmac, randomBytes } from "crypto";
|
|
32194
32360
|
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
|
|
32195
32361
|
import { join as join8 } from "path";
|
|
32196
32362
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
32197
32363
|
|
|
32198
|
-
// ../../packages/persistence/src/history-
|
|
32364
|
+
// ../../packages/persistence/src/history-backfill.ts
|
|
32199
32365
|
import { existsSync as existsSync4 } from "fs";
|
|
32200
32366
|
import { join as join9 } from "path";
|
|
32367
|
+
|
|
32368
|
+
// ../../packages/persistence/src/history-preview.ts
|
|
32369
|
+
import { existsSync as existsSync5 } from "fs";
|
|
32370
|
+
import { join as join10 } from "path";
|
|
32201
32371
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
32202
32372
|
|
|
32203
32373
|
// ../../packages/persistence/src/store-symlinks.ts
|
|
32204
|
-
import { existsSync as
|
|
32205
|
-
import { dirname as dirname3, join as
|
|
32374
|
+
import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
|
|
32375
|
+
import { dirname as dirname3, join as join11, resolve } from "path";
|
|
32206
32376
|
|
|
32207
32377
|
// ../../packages/persistence/src/vault/crypto.ts
|
|
32208
32378
|
import {
|
|
@@ -32217,18 +32387,18 @@ import {
|
|
|
32217
32387
|
import { execFileSync } from "child_process";
|
|
32218
32388
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
32219
32389
|
import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
32220
|
-
import { join as
|
|
32390
|
+
import { join as join12 } from "path";
|
|
32221
32391
|
|
|
32222
32392
|
// ../../packages/persistence/src/vault/vault.ts
|
|
32223
32393
|
import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
|
|
32224
32394
|
|
|
32225
32395
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
32226
|
-
import { existsSync as
|
|
32227
|
-
import { join as
|
|
32396
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
32397
|
+
import { join as join13 } from "path";
|
|
32228
32398
|
|
|
32229
32399
|
// ../../packages/plugin-sdk/src/config.ts
|
|
32230
|
-
import { existsSync as
|
|
32231
|
-
import { join as
|
|
32400
|
+
import { existsSync as existsSync8 } from "fs";
|
|
32401
|
+
import { join as join14 } from "path";
|
|
32232
32402
|
|
|
32233
32403
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
32234
32404
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
@@ -32282,8 +32452,8 @@ function resolveProvider() {
|
|
|
32282
32452
|
function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
|
|
32283
32453
|
try {
|
|
32284
32454
|
ensureLayoutDirSync(base);
|
|
32285
|
-
const settingsFile =
|
|
32286
|
-
if (
|
|
32455
|
+
const settingsFile = join14(settingsDir(base), "settings.json");
|
|
32456
|
+
if (existsSync8(settingsFile)) tightenFile(settingsFile);
|
|
32287
32457
|
} catch {
|
|
32288
32458
|
}
|
|
32289
32459
|
migrateLegacyLayout(base);
|
|
@@ -32308,7 +32478,7 @@ function resolveProviderSafe(resolveProviderFn) {
|
|
|
32308
32478
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
32309
32479
|
import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
|
|
32310
32480
|
import { homedir as homedir2 } from "os";
|
|
32311
|
-
import { basename as basename3, join as
|
|
32481
|
+
import { basename as basename3, join as join16 } from "path";
|
|
32312
32482
|
|
|
32313
32483
|
// ../../packages/detections/src/egress/registry.ts
|
|
32314
32484
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -35343,24 +35513,20 @@ function maskText(text) {
|
|
|
35343
35513
|
}
|
|
35344
35514
|
|
|
35345
35515
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
35346
|
-
import { existsSync as
|
|
35347
|
-
import { basename as basename2, dirname as dirname4, isAbsolute, join as
|
|
35516
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
|
|
35517
|
+
import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
|
|
35348
35518
|
|
|
35349
35519
|
// ../../packages/plugin-sdk/src/events.ts
|
|
35350
|
-
import { createHash as
|
|
35520
|
+
import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
|
|
35351
35521
|
|
|
35352
35522
|
// ../../packages/plugin-sdk/src/isolated-scan.ts
|
|
35353
|
-
import { existsSync as
|
|
35523
|
+
import { existsSync as existsSync10 } from "fs";
|
|
35354
35524
|
import { fileURLToPath } from "url";
|
|
35355
35525
|
import { Worker } from "worker_threads";
|
|
35356
35526
|
|
|
35357
|
-
// ../../packages/plugin-sdk/src/
|
|
35358
|
-
|
|
35359
|
-
import {
|
|
35360
|
-
import { join as join16 } from "path";
|
|
35361
|
-
|
|
35362
|
-
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
35363
|
-
import { arch, hostname as hostname4, platform, release } from "os";
|
|
35527
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
35528
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
35529
|
+
import { join as join18 } from "path";
|
|
35364
35530
|
|
|
35365
35531
|
// ../../packages/plugin-sdk/src/model-governance.ts
|
|
35366
35532
|
import {
|
|
@@ -35368,16 +35534,42 @@ import {
|
|
|
35368
35534
|
fstatSync,
|
|
35369
35535
|
mkdirSync as mkdirSync2,
|
|
35370
35536
|
openSync as openSync2,
|
|
35371
|
-
readFileSync as
|
|
35537
|
+
readFileSync as readFileSync10,
|
|
35372
35538
|
readSync,
|
|
35373
35539
|
writeFileSync as writeFileSync5
|
|
35374
35540
|
} from "fs";
|
|
35375
35541
|
import { join as join17 } from "path";
|
|
35376
35542
|
var TAIL_BYTES = 256 * 1024;
|
|
35377
35543
|
|
|
35544
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
35545
|
+
var HOST_FEATURE = {
|
|
35546
|
+
ModelSwitch: "model-switch",
|
|
35547
|
+
VaultPointerDisplay: "vault-pointer-display"
|
|
35548
|
+
};
|
|
35549
|
+
var HOST_FLOORS = {
|
|
35550
|
+
[HOST_FEATURE.ModelSwitch]: {
|
|
35551
|
+
label: "model-switch protection",
|
|
35552
|
+
hookEvents: ["PreModelSwitch", "PostModelSwitch"],
|
|
35553
|
+
since: "2.1.251"
|
|
35554
|
+
},
|
|
35555
|
+
[HOST_FEATURE.VaultPointerDisplay]: {
|
|
35556
|
+
label: "vault pointer display",
|
|
35557
|
+
hookEvents: ["MessageDisplay"],
|
|
35558
|
+
since: "2.1.152"
|
|
35559
|
+
}
|
|
35560
|
+
};
|
|
35561
|
+
|
|
35562
|
+
// ../../packages/plugin-sdk/src/ignore-layers.ts
|
|
35563
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
35564
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
35565
|
+
import { join as join19 } from "path";
|
|
35566
|
+
|
|
35567
|
+
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
35568
|
+
import { arch, hostname as hostname4, platform, release } from "os";
|
|
35569
|
+
|
|
35378
35570
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
35379
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
35380
|
-
import { join as
|
|
35571
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
|
|
35572
|
+
import { join as join20 } from "path";
|
|
35381
35573
|
|
|
35382
35574
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
35383
35575
|
import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
|
|
@@ -35394,8 +35586,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
|
35394
35586
|
}
|
|
35395
35587
|
|
|
35396
35588
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
35397
|
-
import { existsSync as
|
|
35398
|
-
import { basename as basename5, join as
|
|
35589
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
|
|
35590
|
+
import { basename as basename5, join as join21 } from "path";
|
|
35399
35591
|
|
|
35400
35592
|
// ../../packages/plugin-sdk/src/provider-env-antigravity.ts
|
|
35401
35593
|
var optionalBaseUrl2 = external_exports.preprocess((v) => {
|
|
@@ -35513,11 +35705,11 @@ async function applySetupTriageSuppressions(entries, writer, opts) {
|
|
|
35513
35705
|
|
|
35514
35706
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
35515
35707
|
import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
35516
|
-
import { join as
|
|
35708
|
+
import { join as join22 } from "path";
|
|
35517
35709
|
|
|
35518
35710
|
// ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
|
|
35519
35711
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
35520
|
-
import { join as
|
|
35712
|
+
import { join as join23 } from "path";
|
|
35521
35713
|
|
|
35522
35714
|
// ../../packages/setup-wizard/src/triage/dedupe.ts
|
|
35523
35715
|
function dedupeKey(hit) {
|
|
@@ -35572,12 +35764,12 @@ function deriveFalsePositivePatterns(hits, rec, plan) {
|
|
|
35572
35764
|
}
|
|
35573
35765
|
|
|
35574
35766
|
// ../../packages/setup-wizard/src/triage/gate-display.ts
|
|
35575
|
-
function findContext(entry,
|
|
35576
|
-
const byFingerprint =
|
|
35767
|
+
function findContext(entry, join27) {
|
|
35768
|
+
const byFingerprint = join27.find(
|
|
35577
35769
|
(j) => j.valueFingerprint !== void 0 && j.valueFingerprint === entry.valueFingerprint
|
|
35578
35770
|
);
|
|
35579
35771
|
if (byFingerprint) return byFingerprint.maskedContext;
|
|
35580
|
-
const byRuleAndMask =
|
|
35772
|
+
const byRuleAndMask = join27.find(
|
|
35581
35773
|
(j) => j.ruleId === entry.ruleId && j.maskedMatch === entry.maskedValue
|
|
35582
35774
|
);
|
|
35583
35775
|
return byRuleAndMask?.maskedContext;
|
|
@@ -35648,13 +35840,13 @@ function renderShowcase(showcase) {
|
|
|
35648
35840
|
|
|
35649
35841
|
${blocks.join("\n\n")}`;
|
|
35650
35842
|
}
|
|
35651
|
-
function renderSuppressionGate(entries,
|
|
35843
|
+
function renderSuppressionGate(entries, join27) {
|
|
35652
35844
|
if (entries.length === 0) {
|
|
35653
35845
|
return "No false-positive suppressions to confirm \u2014 nothing will be written.";
|
|
35654
35846
|
}
|
|
35655
35847
|
const header = entries.length === 1 ? "This looks like a false positive \u2014 take a look before I suppress it:" : `These ${String(entries.length)} look like false positives \u2014 take a look before I suppress them:`;
|
|
35656
35848
|
const blocks = entries.map((entry, i) => {
|
|
35657
|
-
const context = findContext(entry,
|
|
35849
|
+
const context = findContext(entry, join27);
|
|
35658
35850
|
const lines = [
|
|
35659
35851
|
`${String(i + 1)}. ${entry.ruleId} [${entry.category}]`,
|
|
35660
35852
|
` value: ${entry.maskedValue}`,
|
|
@@ -35740,9 +35932,9 @@ function mergeRecommendations(verdicts) {
|
|
|
35740
35932
|
}
|
|
35741
35933
|
|
|
35742
35934
|
// ../../packages/setup-wizard/src/triage/plan-file.ts
|
|
35743
|
-
import { mkdtempSync, readFileSync as
|
|
35935
|
+
import { mkdtempSync, readFileSync as readFileSync14, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
35744
35936
|
import { tmpdir } from "os";
|
|
35745
|
-
import { basename as basename6, dirname as dirname6, join as
|
|
35937
|
+
import { basename as basename6, dirname as dirname6, join as join24 } from "path";
|
|
35746
35938
|
var SuppressionEntrySchema = external_exports.object({
|
|
35747
35939
|
ruleId: external_exports.string(),
|
|
35748
35940
|
category: DetectionCategory,
|
|
@@ -35797,13 +35989,13 @@ function serializePlan(plan, current) {
|
|
|
35797
35989
|
function writePlanFile(plan, current, rawValues, deps = {}) {
|
|
35798
35990
|
const serialized = serializePlan(plan, current);
|
|
35799
35991
|
assertRawFree(serialized, rawValues);
|
|
35800
|
-
const dir = (deps.mkTempDir ?? (() => mkdtempSync(
|
|
35801
|
-
const path =
|
|
35992
|
+
const dir = (deps.mkTempDir ?? (() => mkdtempSync(join24(tmpdir(), "aka-plan-"))))();
|
|
35993
|
+
const path = join24(dir, "setup-plan.json");
|
|
35802
35994
|
writeFileSync9(path, serialized, { encoding: "utf8", mode: 384 });
|
|
35803
35995
|
return path;
|
|
35804
35996
|
}
|
|
35805
35997
|
function readPlanFile(path) {
|
|
35806
|
-
const text =
|
|
35998
|
+
const text = readFileSync14(path, "utf8");
|
|
35807
35999
|
const json2 = JSON.parse(text);
|
|
35808
36000
|
return PersistedPlanSchema.parse(json2);
|
|
35809
36001
|
}
|
|
@@ -35877,8 +36069,8 @@ function buildJoinEntries(hits) {
|
|
|
35877
36069
|
}
|
|
35878
36070
|
|
|
35879
36071
|
// ../../packages/setup-wizard/src/triage/resolve.ts
|
|
35880
|
-
function resolveSuppressions(rec,
|
|
35881
|
-
const byId = new Map(
|
|
36072
|
+
function resolveSuppressions(rec, join27) {
|
|
36073
|
+
const byId = new Map(join27.map((e) => [e.id, e]));
|
|
35882
36074
|
const entries = [];
|
|
35883
36075
|
const skipped = [];
|
|
35884
36076
|
for (const cat of rec.perCategory) {
|
|
@@ -35980,7 +36172,7 @@ function parseTriageStream(text) {
|
|
|
35980
36172
|
return { hits, status: "complete" };
|
|
35981
36173
|
}
|
|
35982
36174
|
function planTriageWriteback(hits, rec) {
|
|
35983
|
-
const
|
|
36175
|
+
const join27 = buildJoinEntries(hits);
|
|
35984
36176
|
const rawValues = hits.map((h) => h.rawMatch);
|
|
35985
36177
|
const skipped = [];
|
|
35986
36178
|
const posture = {};
|
|
@@ -36020,7 +36212,7 @@ function planTriageWriteback(hits, rec) {
|
|
|
36020
36212
|
}
|
|
36021
36213
|
const { entries, skipped: resolveSkips } = resolveSuppressions(
|
|
36022
36214
|
{ perCategory: safeCategories, notes: rec.notes },
|
|
36023
|
-
|
|
36215
|
+
join27
|
|
36024
36216
|
);
|
|
36025
36217
|
skipped.push(...resolveSkips);
|
|
36026
36218
|
let notes = rec.notes;
|
|
@@ -36030,7 +36222,7 @@ function planTriageWriteback(hits, rec) {
|
|
|
36030
36222
|
if (err instanceof RawEgressError) notes = SCRUBBED_NOTES;
|
|
36031
36223
|
else throw err;
|
|
36032
36224
|
}
|
|
36033
|
-
return { entries, posture, showcase, join:
|
|
36225
|
+
return { entries, posture, showcase, join: join27, notes, skipped };
|
|
36034
36226
|
}
|
|
36035
36227
|
function recommendedPosture(evidence) {
|
|
36036
36228
|
return { ...severityFloorPosture(), ...evidence };
|
|
@@ -36299,9 +36491,9 @@ function parseRecommendation(text) {
|
|
|
36299
36491
|
|
|
36300
36492
|
// src/triage/judge.ts
|
|
36301
36493
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
36302
|
-
import { mkdtempSync as mkdtempSync2, readFileSync as
|
|
36494
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, rmSync as rmSync8 } from "fs";
|
|
36303
36495
|
import { tmpdir as tmpdir2 } from "os";
|
|
36304
|
-
import { dirname as dirname7, join as
|
|
36496
|
+
import { dirname as dirname7, join as join25 } from "path";
|
|
36305
36497
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
36306
36498
|
|
|
36307
36499
|
// ../../packages/plugin-sdk/src/bare-command.ts
|
|
@@ -36413,7 +36605,7 @@ function planBareCommand(command, args, deps = {}) {
|
|
|
36413
36605
|
|
|
36414
36606
|
// src/triage/judge.ts
|
|
36415
36607
|
var TRIAGE_DIR = dirname7(fileURLToPath2(import.meta.url));
|
|
36416
|
-
var DEFAULT_RUBRIC_PATH =
|
|
36608
|
+
var DEFAULT_RUBRIC_PATH = join25(
|
|
36417
36609
|
TRIAGE_DIR,
|
|
36418
36610
|
"..",
|
|
36419
36611
|
"..",
|
|
@@ -36450,7 +36642,7 @@ function judgeEnv(platform2 = process.platform) {
|
|
|
36450
36642
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
|
36451
36643
|
};
|
|
36452
36644
|
if (platform2 === "darwin") {
|
|
36453
|
-
env.CLAUDE_CONFIG_DIR = mkdtempSync2(
|
|
36645
|
+
env.CLAUDE_CONFIG_DIR = mkdtempSync2(join25(tmpdir2(), "aka-judge-cfg-"));
|
|
36454
36646
|
}
|
|
36455
36647
|
return env;
|
|
36456
36648
|
}
|
|
@@ -36485,7 +36677,7 @@ function runJudge(hits, deps) {
|
|
|
36485
36677
|
if (typeof deps.spawn !== "function") {
|
|
36486
36678
|
throw new TypeError("runJudge requires deps.spawn \u2014 there is no live-spawn fallback");
|
|
36487
36679
|
}
|
|
36488
|
-
const rubric = deps.loadRubric?.() ??
|
|
36680
|
+
const rubric = deps.loadRubric?.() ?? readFileSync15(DEFAULT_RUBRIC_PATH, "utf8");
|
|
36489
36681
|
const hitsJsonl = hits.map((h) => JSON.stringify(toJudgePayload(h))).join("\n");
|
|
36490
36682
|
const fullPrompt = `${rubric}
|
|
36491
36683
|
|
|
@@ -36755,10 +36947,10 @@ function resolveCreatedBy() {
|
|
|
36755
36947
|
}
|
|
36756
36948
|
function loadRubric() {
|
|
36757
36949
|
const here = dirname8(fileURLToPath4(import.meta.url));
|
|
36758
|
-
const shipped =
|
|
36759
|
-
if (
|
|
36760
|
-
return
|
|
36761
|
-
|
|
36950
|
+
const shipped = join26(here, "triage-rubric.md");
|
|
36951
|
+
if (existsSync12(shipped)) return readFileSync16(shipped, "utf8");
|
|
36952
|
+
return readFileSync16(
|
|
36953
|
+
join26(here, "..", "..", "..", "packages", "setup-wizard", "assets", "triage-rubric.md"),
|
|
36762
36954
|
"utf8"
|
|
36763
36955
|
);
|
|
36764
36956
|
}
|
|
@@ -36768,7 +36960,7 @@ async function main() {
|
|
|
36768
36960
|
argv,
|
|
36769
36961
|
// fd 0 = stdin; the wizard pipes `backfill.js --triage` into this on preview.
|
|
36770
36962
|
// Called only on the preview path — the confirm path never reads a stream.
|
|
36771
|
-
readStream: (streamPath) => streamPath !== void 0 ?
|
|
36963
|
+
readStream: (streamPath) => streamPath !== void 0 ? readFileSync16(streamPath, "utf8") : readFileSync16(0, "utf8"),
|
|
36772
36964
|
runJudge: (hits) => runJudge(hits, { spawn: spawnClaude, loadRubric }),
|
|
36773
36965
|
// The distinct model-judge egress consent, read from settings.json. When it
|
|
36774
36966
|
// is absent or stale the preview skips the judge instead of sending findings
|