@akasecurity/ai-tc-claude-code 0.9.10 → 0.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +5 -5
- package/scripts/apply-suppressions.js +622 -430
- package/scripts/backfill.js +732 -504
- package/scripts/filescan.js +726 -496
- package/scripts/firstrun.js +772 -516
- package/scripts/history-sync.js +611 -418
- package/scripts/intro.js +240 -90
- package/scripts/message-display.js +604 -412
- package/scripts/onboard.js +601 -408
- package/scripts/post-model-switch.js +258 -110
- package/scripts/post-tool-use.js +880 -493
- package/scripts/pre-model-switch.js +691 -469
- package/scripts/pre-tool-use.js +728 -498
- package/scripts/query.js +845 -526
- package/scripts/reconcile.js +716 -494
- package/scripts/remediate.js +728 -498
- package/scripts/scan-worker.js +153 -58
- package/scripts/session-start.js +783 -558
- package/scripts/start-light.js +238 -88
- package/scripts/statusline.js +697 -475
- package/scripts/stop.js +490 -121
- package/scripts/sync.js +722 -494
- package/scripts/user-prompt-submit.js +725 -495
package/scripts/query.js
CHANGED
|
@@ -20420,6 +20420,101 @@ function epochMillisToIso(ms) {
|
|
|
20420
20420
|
return new Date(ms).toISOString();
|
|
20421
20421
|
}
|
|
20422
20422
|
|
|
20423
|
+
// ../../packages/schema/src/security/recommendations.ts
|
|
20424
|
+
var SEVERITY_WEIGHT = {
|
|
20425
|
+
critical: 4,
|
|
20426
|
+
high: 3,
|
|
20427
|
+
medium: 2,
|
|
20428
|
+
low: 1
|
|
20429
|
+
};
|
|
20430
|
+
var SEVERITY_WEIGHT_BY_STRING = SEVERITY_WEIGHT;
|
|
20431
|
+
function severityWeight(severity) {
|
|
20432
|
+
return SEVERITY_WEIGHT_BY_STRING[severity] ?? 0;
|
|
20433
|
+
}
|
|
20434
|
+
var ADVICE = {
|
|
20435
|
+
secret: "Rotate the exposed credentials and move them out of prompts (secrets manager / env vars).",
|
|
20436
|
+
pii: "Remove or mask personal data before it reaches the model.",
|
|
20437
|
+
financial: "Strip card and account numbers; share only non-sensitive references.",
|
|
20438
|
+
phi: "Remove protected health information \u2014 it should never reach an external model.",
|
|
20439
|
+
code_context: "Confirm this proprietary code context is safe to share.",
|
|
20440
|
+
code_flaw: "Review the flagged pattern and apply the secure alternative (parameterized queries, safe deserializers, etc.).",
|
|
20441
|
+
config: "Review the setting \u2014 a hook conflict or an egress change applies to every session that follows.",
|
|
20442
|
+
custom: "Review against your organization\u2019s custom policy."
|
|
20443
|
+
};
|
|
20444
|
+
var REC_TEMPLATE = {
|
|
20445
|
+
secret: { title: "Exposed secret detected", action: "Rotate" },
|
|
20446
|
+
pii: { title: "Personal data in a prompt", action: "Remove" },
|
|
20447
|
+
financial: { title: "Financial data detected", action: "Strip" },
|
|
20448
|
+
phi: { title: "Health information detected", action: "Remove" },
|
|
20449
|
+
code_context: { title: "Proprietary code shared", action: "Review" },
|
|
20450
|
+
code_flaw: { title: "Insecure code pattern", action: "Fix" },
|
|
20451
|
+
config: { title: "Weakened configuration", action: "Review" },
|
|
20452
|
+
custom: { title: "Custom policy match", action: "Review" }
|
|
20453
|
+
};
|
|
20454
|
+
var MAX_RECOMMENDATIONS = 10;
|
|
20455
|
+
function bucketizeRecommendations(findings) {
|
|
20456
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
20457
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
20458
|
+
for (const f of findings) {
|
|
20459
|
+
const n = f.count ?? 1;
|
|
20460
|
+
byRule.set(f.ruleId, (byRule.get(f.ruleId) ?? 0) + n);
|
|
20461
|
+
const b = buckets.get(f.category) ?? {
|
|
20462
|
+
category: f.category,
|
|
20463
|
+
count: 0,
|
|
20464
|
+
categoryCount: 0,
|
|
20465
|
+
severity: f.severity,
|
|
20466
|
+
weight: 0,
|
|
20467
|
+
ruleId: f.ruleId
|
|
20468
|
+
};
|
|
20469
|
+
b.categoryCount += n;
|
|
20470
|
+
const w = severityWeight(f.severity);
|
|
20471
|
+
if (w > b.weight) {
|
|
20472
|
+
b.weight = w;
|
|
20473
|
+
b.severity = f.severity;
|
|
20474
|
+
b.ruleId = f.ruleId;
|
|
20475
|
+
}
|
|
20476
|
+
buckets.set(f.category, b);
|
|
20477
|
+
}
|
|
20478
|
+
for (const b of buckets.values()) b.count = byRule.get(b.ruleId) ?? 0;
|
|
20479
|
+
return [...buckets.values()].sort((a, b) => b.weight - a.weight || b.categoryCount - a.categoryCount).slice(0, MAX_RECOMMENDATIONS);
|
|
20480
|
+
}
|
|
20481
|
+
function buildRecommendations(findings) {
|
|
20482
|
+
return bucketizeRecommendations(findings).map((b) => {
|
|
20483
|
+
const copy = recommendationCopy(b.category);
|
|
20484
|
+
return {
|
|
20485
|
+
severity: b.severity,
|
|
20486
|
+
title: copy.title,
|
|
20487
|
+
description: copy.advice,
|
|
20488
|
+
context: `${b.ruleId} \xB7 ${String(b.count)} finding${b.count === 1 ? "" : "s"}`,
|
|
20489
|
+
action: copy.action
|
|
20490
|
+
};
|
|
20491
|
+
});
|
|
20492
|
+
}
|
|
20493
|
+
function recommendationCopy(category) {
|
|
20494
|
+
const template = REC_TEMPLATE_BY_STRING[category] ?? {
|
|
20495
|
+
title: `${category} finding`,
|
|
20496
|
+
action: "Review"
|
|
20497
|
+
};
|
|
20498
|
+
return {
|
|
20499
|
+
...template,
|
|
20500
|
+
advice: ADVICE_BY_STRING[category] ?? "Review this finding against your policy."
|
|
20501
|
+
};
|
|
20502
|
+
}
|
|
20503
|
+
var ADVICE_BY_STRING = ADVICE;
|
|
20504
|
+
var REC_TEMPLATE_BY_STRING = REC_TEMPLATE;
|
|
20505
|
+
function healthScore(summary) {
|
|
20506
|
+
const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
|
|
20507
|
+
const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
|
|
20508
|
+
return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
|
|
20509
|
+
}
|
|
20510
|
+
function findingStatus(summary) {
|
|
20511
|
+
return {
|
|
20512
|
+
score: healthScore(summary),
|
|
20513
|
+
unreviewed: { ...summary.bySeverity },
|
|
20514
|
+
openFindings: summary.findings
|
|
20515
|
+
};
|
|
20516
|
+
}
|
|
20517
|
+
|
|
20423
20518
|
// ../../packages/schema/src/token/cost-model.ts
|
|
20424
20519
|
var PROVIDER_PLATFORM = /* @__PURE__ */ new Map([
|
|
20425
20520
|
["anthropic", "anthropic"],
|
|
@@ -20799,13 +20894,11 @@ var FindingGroup = external_exports.object({
|
|
|
20799
20894
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20800
20895
|
instances: external_exports.array(FindingInstance),
|
|
20801
20896
|
// Derived from instances' statuses with open-dominates precedence (see
|
|
20802
|
-
//
|
|
20897
|
+
// foldGroupStatus). Undefined only when no instance carries a status.
|
|
20803
20898
|
status: FindingStatus.optional(),
|
|
20804
|
-
// The distinct people across the WHOLE group, not just the
|
|
20805
|
-
//
|
|
20806
|
-
//
|
|
20807
|
-
// instance carries a user, or when the store supplied whole-group folds
|
|
20808
|
-
// without one.
|
|
20899
|
+
// The distinct people across the WHOLE group, not just the instances
|
|
20900
|
+
// carried here. Undefined when no instance carries a user, or when the
|
|
20901
|
+
// store supplied whole-group folds without one.
|
|
20809
20902
|
users: external_exports.array(FindingUser).optional()
|
|
20810
20903
|
}).meta({ id: "FindingGroup" });
|
|
20811
20904
|
var FindingStats = external_exports.object({
|
|
@@ -20834,21 +20927,31 @@ var FindingFacets = external_exports.object({
|
|
|
20834
20927
|
// counted under no value.
|
|
20835
20928
|
status: external_exports.array(FindingFacetItem),
|
|
20836
20929
|
// Host tool (attributes.tool_name). Present only on the instance-level
|
|
20837
|
-
// reads, which can filter by it; the
|
|
20930
|
+
// reads, which can filter by it; the type-level read omits the dimension
|
|
20838
20931
|
// because a group spans tools.
|
|
20839
20932
|
tool: external_exports.array(FindingFacetItem).optional()
|
|
20840
20933
|
}).meta({ id: "FindingFacets" });
|
|
20841
|
-
var
|
|
20842
|
-
|
|
20934
|
+
var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
|
|
20935
|
+
id: "FindingTypeSummary"
|
|
20936
|
+
});
|
|
20937
|
+
var DEFAULT_FINDING_TYPES_LIMIT = 50;
|
|
20938
|
+
var MAX_FINDING_TYPES_LIMIT = 100;
|
|
20939
|
+
var ListFindingTypesQuery = external_exports.object({
|
|
20843
20940
|
// NOTE: severity filters by Severity (critical/high/medium/low), not by
|
|
20844
|
-
// FindingAction.
|
|
20941
|
+
// FindingAction. It narrows TYPES: a type's severity is the one its newest
|
|
20942
|
+
// firing version carries, and this list pages types.
|
|
20943
|
+
//
|
|
20944
|
+
// That is NOT a claim the findings of a type share it. A rule can hold several
|
|
20945
|
+
// definition versions at different severities, so a type kept by this filter
|
|
20946
|
+
// can hold findings that individually do not match — see totals.findings on
|
|
20947
|
+
// ListFindingTypesResponse, which counts them all.
|
|
20845
20948
|
severity: external_exports.array(Severity).optional(),
|
|
20846
20949
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20847
20950
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20848
20951
|
action: external_exports.array(FindingAction).optional(),
|
|
20849
|
-
// Matches a
|
|
20850
|
-
// individual
|
|
20851
|
-
//
|
|
20952
|
+
// Matches a type's DERIVED status (see FindingGroup.status), not its
|
|
20953
|
+
// individual findings' — so a filtered row's status always reads one of the
|
|
20954
|
+
// requested values.
|
|
20852
20955
|
status: external_exports.array(FindingStatus).optional(),
|
|
20853
20956
|
q: external_exports.string().optional(),
|
|
20854
20957
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
@@ -20858,23 +20961,37 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
20858
20961
|
// from a time-scoped page (Activity's range) can carry that scope. Absent
|
|
20859
20962
|
// means all time — this list has no default window.
|
|
20860
20963
|
from: external_exports.iso.datetime().optional(),
|
|
20861
|
-
// A
|
|
20862
|
-
//
|
|
20863
|
-
//
|
|
20864
|
-
//
|
|
20865
|
-
//
|
|
20964
|
+
// A RULE id that must appear in the page even when the cursor has already
|
|
20965
|
+
// advanced past its sort position. This is what keeps the selected type
|
|
20966
|
+
// visible in the list once it paginates: the target is appended out of sort
|
|
20967
|
+
// order rather than scanned forward for. Never affects totals, facets or the
|
|
20968
|
+
// cursor. Unlike the grouped read this replaces, it names a rule only — an
|
|
20969
|
+
// instance id is resolved by `findingInstance`, which is a primary-key seek
|
|
20970
|
+
// and so is not bounded by what any page happens to hold.
|
|
20866
20971
|
includeId: external_exports.string().optional(),
|
|
20867
|
-
|
|
20868
|
-
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
20972
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
|
|
20869
20973
|
cursor: external_exports.string().optional()
|
|
20870
20974
|
});
|
|
20871
|
-
var
|
|
20975
|
+
var ListFindingTypesResponse = external_exports.object({
|
|
20872
20976
|
totals: external_exports.object({
|
|
20977
|
+
// Findings belonging to the matching TYPES — not findings that each match
|
|
20978
|
+
// the filters. The filters here select types, so a type that survives
|
|
20979
|
+
// contributes its whole instanceCount.
|
|
20980
|
+
//
|
|
20981
|
+
// `status` is the one exception, narrowed per finding via
|
|
20982
|
+
// countInstancesByStatus. `severity`, `provider` and `action` are not, so
|
|
20983
|
+
// this can exceed what the instance read reports for the same filters: a
|
|
20984
|
+
// rule whose severity moved between versions is kept on its newest and
|
|
20985
|
+
// still counts its older findings. Narrowing the other three needs
|
|
20986
|
+
// per-dimension counts the aggregate does not carry today.
|
|
20873
20987
|
findings: external_exports.number().int().nonnegative(),
|
|
20874
|
-
|
|
20988
|
+
// Counts TYPES, which is the unit this read pages. The instance read's
|
|
20989
|
+
// own totals count findings; the two deliberately answer different
|
|
20990
|
+
// questions and are never summed.
|
|
20991
|
+
types: external_exports.number().int().nonnegative()
|
|
20875
20992
|
}),
|
|
20876
20993
|
facets: FindingFacets,
|
|
20877
|
-
items: external_exports.array(
|
|
20994
|
+
items: external_exports.array(FindingTypeSummary),
|
|
20878
20995
|
nextCursor: external_exports.string().nullable(),
|
|
20879
20996
|
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
20880
20997
|
// many times that rule fired in the session's persisted transcript. Findings
|
|
@@ -20882,7 +20999,7 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
20882
20999
|
// every firing, so the two numbers legitimately differ — this map lets a
|
|
20883
21000
|
// session-scoped view show both.
|
|
20884
21001
|
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
20885
|
-
}).meta({ id: "
|
|
21002
|
+
}).meta({ id: "ListFindingTypesResponse" });
|
|
20886
21003
|
var ApplyFindingActionRequest = external_exports.object({
|
|
20887
21004
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
20888
21005
|
// it, so it is excluded from the request contract. The mapping helper
|
|
@@ -20912,12 +21029,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
|
|
|
20912
21029
|
var MAX_FLAT_FINDINGS_LIMIT = 200;
|
|
20913
21030
|
var ListFindingInstancesQuery = external_exports.object({
|
|
20914
21031
|
severity: external_exports.array(Severity).optional(),
|
|
20915
|
-
// Rule ids, the same vocabulary the
|
|
21032
|
+
// Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
|
|
21033
|
+
// ONE of them is how the master/detail view scopes its right-hand panel.
|
|
20916
21034
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20917
21035
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20918
21036
|
action: external_exports.array(FindingAction).optional(),
|
|
20919
21037
|
// Matches each instance's OWN derived status (deriveFindingStatus), unlike
|
|
20920
|
-
// the
|
|
21038
|
+
// the types query's type-level fold.
|
|
20921
21039
|
status: external_exports.array(FindingStatus).optional(),
|
|
20922
21040
|
// Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
|
|
20923
21041
|
// where the free-text `q` can only match the rendered "via Bash" label.
|
|
@@ -20934,37 +21052,47 @@ var ListFindingInstancesQuery = external_exports.object({
|
|
|
20934
21052
|
});
|
|
20935
21053
|
var ListFindingInstancesResponse = external_exports.object({
|
|
20936
21054
|
// Instances matching the filters across the whole scope, not just this
|
|
20937
|
-
// page — cursor-independent, like the
|
|
21055
|
+
// page — cursor-independent, like the types list's totals.
|
|
20938
21056
|
totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
|
|
20939
|
-
// Counts in INSTANCES here, where the
|
|
21057
|
+
// Counts in INSTANCES here, where the types response counts types. Each
|
|
20940
21058
|
// dimension still excludes its own filter.
|
|
20941
21059
|
facets: FindingFacets,
|
|
20942
21060
|
items: external_exports.array(FindingInstanceDetail),
|
|
20943
21061
|
nextCursor: external_exports.string().nullable()
|
|
20944
21062
|
}).meta({ id: "ListFindingInstancesResponse" });
|
|
20945
|
-
var
|
|
20946
|
-
//
|
|
20947
|
-
//
|
|
20948
|
-
|
|
20949
|
-
|
|
20950
|
-
|
|
20951
|
-
|
|
20952
|
-
// Folded from the instances' derived statuses with the same
|
|
20953
|
-
// open-dominates precedence a group uses.
|
|
20954
|
-
status: FindingStatus.optional(),
|
|
20955
|
-
// Distinct rules seen at this location, capped — the row shows them as
|
|
20956
|
-
// chips, and the count is what conveys scale.
|
|
20957
|
-
ruleIds: external_exports.array(external_exports.string())
|
|
20958
|
-
}).meta({ id: "FindingLocationFile" });
|
|
20959
|
-
var FindingLocationRepo = external_exports.object({
|
|
21063
|
+
var FindingLocationSummary = external_exports.object({
|
|
21064
|
+
// Opaque, stable, minted from the pair by encodeLocationId. It exists
|
|
21065
|
+
// because a location's identity is two values and a URL param carries one:
|
|
21066
|
+
// `?loc=` names a location the way `?rule=` names a type. Only ever compared
|
|
21067
|
+
// for EQUALITY — the page's selection check, this read's `includeId`, the
|
|
21068
|
+
// client's page dedupe — never decoded, and never a sort key.
|
|
21069
|
+
id: external_exports.string(),
|
|
20960
21070
|
/** Empty when the instances carried no repo attribute. */
|
|
20961
21071
|
repo: external_exports.string(),
|
|
21072
|
+
// Empty when the instances carried no file path (a prompt, or a tool call
|
|
21073
|
+
// with no file attribution). Both halves empty is a real location — usually
|
|
21074
|
+
// the largest one in a store — and is selectable like any other.
|
|
21075
|
+
file: external_exports.string(),
|
|
20962
21076
|
instanceCount: external_exports.number().int().nonnegative(),
|
|
21077
|
+
// The WORST severity present, not the first row's. It is this list's primary
|
|
21078
|
+
// sort key, so it is also what explains why a row is where it is, and it is
|
|
21079
|
+
// how a reader decides what to open without opening everything.
|
|
20963
21080
|
maxSeverity: Severity,
|
|
20964
21081
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
21082
|
+
// Folded from the instances' derived statuses with the same open-dominates
|
|
21083
|
+
// precedence a group uses, so it answers "is anything left to do here" and
|
|
21084
|
+
// not much more: a location holding 1 open among 40 resolved reads like one
|
|
21085
|
+
// holding 40 open. That loss is accepted — the panel beside this list
|
|
21086
|
+
// carries each finding's own status, and instanceCount sits next to the
|
|
21087
|
+
// badge.
|
|
20965
21088
|
status: FindingStatus.optional(),
|
|
20966
|
-
|
|
20967
|
-
|
|
21089
|
+
// Every distinct rule seen at this location, UNCAPPED — so the length is a
|
|
21090
|
+
// tally rather than a sample and a row can say how many there are. Bounded
|
|
21091
|
+
// by the ruleset, not by the store. The view bounds what it DISPLAYS.
|
|
21092
|
+
ruleIds: external_exports.array(external_exports.string())
|
|
21093
|
+
}).meta({ id: "FindingLocationSummary" });
|
|
21094
|
+
var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
|
|
21095
|
+
var MAX_FINDING_LOCATIONS_LIMIT = 100;
|
|
20968
21096
|
var ListFindingLocationsQuery = external_exports.object({
|
|
20969
21097
|
severity: external_exports.array(Severity).optional(),
|
|
20970
21098
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
@@ -20977,18 +21105,42 @@ var ListFindingLocationsQuery = external_exports.object({
|
|
|
20977
21105
|
q: external_exports.string().optional(),
|
|
20978
21106
|
sessionId: external_exports.string().optional(),
|
|
20979
21107
|
from: external_exports.iso.datetime().optional(),
|
|
20980
|
-
|
|
21108
|
+
// A LOCATION id (see FindingLocationSummary.id) that must appear in the page
|
|
21109
|
+
// even when the cursor has already advanced past its sort position — the
|
|
21110
|
+
// counterpart of ListFindingTypesQuery.includeId, and needed far more often
|
|
21111
|
+
// here. Selecting a row pushes the URL, which re-renders the server and resets
|
|
21112
|
+
// the client's page cache to page 0; with distinct (repo, file) pairs running
|
|
21113
|
+
// into the thousands, a selection sitting off page 0 is the ordinary case
|
|
21114
|
+
// rather than a deep-link corner. Never affects totals, facets or the cursor.
|
|
21115
|
+
includeId: external_exports.string().optional(),
|
|
21116
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
|
|
21117
|
+
cursor: external_exports.string().optional()
|
|
20981
21118
|
});
|
|
20982
21119
|
var ListFindingLocationsResponse = external_exports.object({
|
|
20983
21120
|
totals: external_exports.object({
|
|
21121
|
+
// Findings matching the filters across the whole scope. Unlike the types
|
|
21122
|
+
// read's same-named field this needs no caveat: the filters here narrow
|
|
21123
|
+
// per finding, so this is the sum of every row's instanceCount.
|
|
20984
21124
|
findings: external_exports.number().int().nonnegative(),
|
|
20985
|
-
|
|
20986
|
-
|
|
21125
|
+
// Counts LOCATIONS, the unit this read pages — the number the paginator
|
|
21126
|
+
// states. The facets beside it count FINDINGS (see below); a surface
|
|
21127
|
+
// showing both says which is which.
|
|
21128
|
+
locations: external_exports.number().int().nonnegative()
|
|
20987
21129
|
}),
|
|
20988
|
-
|
|
20989
|
-
|
|
20990
|
-
|
|
20991
|
-
|
|
21130
|
+
// Counts in FINDINGS, where the types response counts types, each dimension
|
|
21131
|
+
// still excluding its own filter. Deliberately not locations: counting those
|
|
21132
|
+
// needs a set of location keys per dimension per value — memory tracking the
|
|
21133
|
+
// store times the vocabulary, in a read whose scan promises flat memory —
|
|
21134
|
+
// and the cheap per-location version is not an approximation but WRONG. A
|
|
21135
|
+
// location holding {claudecode, block} and {codex, warn} would survive
|
|
21136
|
+
// provider=claudecode AND action=warn, under which no single finding
|
|
21137
|
+
// matches, so the facet would contradict the instanceCount this whole view
|
|
21138
|
+
// rests on. Findings also keep the toolbar in the same unit as the page
|
|
21139
|
+
// tally and the panel it sits above.
|
|
21140
|
+
facets: FindingFacets,
|
|
21141
|
+
/** Sorted by max severity, then most recent, then (repo, file). */
|
|
21142
|
+
items: external_exports.array(FindingLocationSummary),
|
|
21143
|
+
nextCursor: external_exports.string().nullable()
|
|
20992
21144
|
}).meta({ id: "ListFindingLocationsResponse" });
|
|
20993
21145
|
|
|
20994
21146
|
// ../../packages/schema/src/zod/meta.ts
|
|
@@ -22157,6 +22309,14 @@ var ControlPlaneErrorBody = external_exports.object({
|
|
|
22157
22309
|
message: external_exports.string().optional()
|
|
22158
22310
|
}).optional()
|
|
22159
22311
|
});
|
|
22312
|
+
var RemoteFailureKind = external_exports.enum([
|
|
22313
|
+
"unauthorized",
|
|
22314
|
+
"forbidden",
|
|
22315
|
+
"route-absent",
|
|
22316
|
+
"invalid-request",
|
|
22317
|
+
"rejected",
|
|
22318
|
+
"unreachable"
|
|
22319
|
+
]);
|
|
22160
22320
|
var AttachDeviceRequest = external_exports.object({
|
|
22161
22321
|
// This machine's own continuity id, so re-attaching ROTATES the credential
|
|
22162
22322
|
// on one machine record instead of producing a second one. Client-minted
|
|
@@ -22850,139 +23010,62 @@ function deriveFindingStatus(row) {
|
|
|
22850
23010
|
if (row.latestResolutionStatus === "dismissed") return "dismissed";
|
|
22851
23011
|
return "open";
|
|
22852
23012
|
}
|
|
22853
|
-
function distinctUsers(instances) {
|
|
22854
|
-
const seen = /* @__PURE__ */ new Set();
|
|
22855
|
-
const users = [];
|
|
22856
|
-
for (const i of instances) {
|
|
22857
|
-
if (i.user === void 0 || seen.has(i.user.id)) continue;
|
|
22858
|
-
seen.add(i.user.id);
|
|
22859
|
-
users.push(i.user);
|
|
22860
|
-
}
|
|
22861
|
-
return users;
|
|
22862
|
-
}
|
|
22863
23013
|
function sortUsers(users) {
|
|
22864
23014
|
return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
22865
23015
|
}
|
|
22866
|
-
function
|
|
22867
|
-
const overrides = opts.overrides;
|
|
23016
|
+
function buildFindingTypes(aggregates, opts = {}) {
|
|
22868
23017
|
const packNames = opts.packNames;
|
|
22869
|
-
const
|
|
22870
|
-
const
|
|
22871
|
-
|
|
22872
|
-
const
|
|
22873
|
-
|
|
22874
|
-
else byRuleId.set(row.ruleId, [row]);
|
|
22875
|
-
}
|
|
22876
|
-
const groups = [];
|
|
22877
|
-
for (const [ruleId, ruleRows] of byRuleId) {
|
|
22878
|
-
const instances = ruleRows.map((r) => {
|
|
22879
|
-
const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
|
|
22880
|
-
return {
|
|
22881
|
-
id: r.id,
|
|
22882
|
-
provider: toApiProvider(r.sourceTool),
|
|
22883
|
-
repo: r.repo,
|
|
22884
|
-
file: r.file,
|
|
22885
|
-
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
22886
|
-
...r.eventId === void 0 ? {} : { eventId: r.eventId },
|
|
22887
|
-
...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
|
|
22888
|
-
...r.user === void 0 ? {} : { user: r.user },
|
|
22889
|
-
action: toApiAction(effectiveDbAction),
|
|
22890
|
-
detectedAt: r.occurredAt,
|
|
22891
|
-
confidence: r.confidence,
|
|
22892
|
-
status: r.status
|
|
22893
|
-
};
|
|
22894
|
-
});
|
|
22895
|
-
const agg = aggregates?.get(ruleId);
|
|
22896
|
-
const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
|
|
22897
|
-
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
22898
|
-
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
22899
|
-
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
22900
|
-
);
|
|
22901
|
-
const seenProviders = /* @__PURE__ */ new Set();
|
|
22902
|
-
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
22903
|
-
if (seenProviders.has(p)) return false;
|
|
22904
|
-
seenProviders.add(p);
|
|
22905
|
-
return true;
|
|
22906
|
-
});
|
|
22907
|
-
const actionSet = new Set(
|
|
22908
|
-
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
22909
|
-
);
|
|
23018
|
+
const types = [];
|
|
23019
|
+
for (const [ruleId, agg] of aggregates) {
|
|
23020
|
+
const users = sortUsers(agg.users ?? []);
|
|
23021
|
+
const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
|
|
23022
|
+
const actionSet = new Set(agg.actionsTaken.map(toApiAction));
|
|
22910
23023
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
22911
|
-
const
|
|
22912
|
-
const
|
|
22913
|
-
id: ruleId,
|
|
22914
|
-
name: packNames?.get(ruleId) ?? null
|
|
22915
|
-
};
|
|
22916
|
-
const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
|
|
22917
|
-
const policy = { id: `category:${apiCategory}`, name: apiCategory };
|
|
22918
|
-
const match = {
|
|
22919
|
-
maskedValue: ruleRows[0]?.maskedMatch ?? "",
|
|
22920
|
-
contextPrefix: ""
|
|
22921
|
-
// empty (pending privacy review)
|
|
22922
|
-
};
|
|
22923
|
-
const status = foldGroupStatus(
|
|
22924
|
-
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
22925
|
-
);
|
|
22926
|
-
const group = {
|
|
23024
|
+
const apiCategory = toApiCategory(agg.category ?? "custom");
|
|
23025
|
+
const type = {
|
|
22927
23026
|
id: ruleId,
|
|
22928
23027
|
category: apiCategory,
|
|
22929
23028
|
subtype: ruleId,
|
|
22930
23029
|
// human label comes with pack metadata later
|
|
22931
|
-
severity,
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
|
|
22935
|
-
instanceCount: agg?.instanceCount ?? instances.length,
|
|
23030
|
+
severity: agg.severity ?? "low",
|
|
23031
|
+
detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
|
|
23032
|
+
policy: { id: `category:${apiCategory}`, name: apiCategory },
|
|
23033
|
+
instanceCount: agg.instanceCount,
|
|
22936
23034
|
providers,
|
|
22937
23035
|
aggregateAction,
|
|
22938
|
-
latestDetectedAt,
|
|
22939
|
-
|
|
22940
|
-
status,
|
|
23036
|
+
latestDetectedAt: agg.latestDetectedAt,
|
|
23037
|
+
status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
|
|
22941
23038
|
...users.length > 0 ? { users } : {}
|
|
22942
23039
|
};
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
22947
|
-
}
|
|
23040
|
+
actionsCache.set(type, [...actionSet]);
|
|
23041
|
+
if (agg.searchText !== void 0) {
|
|
23042
|
+
haystackCache.set(type, buildHaystack(type, agg.searchText));
|
|
22948
23043
|
}
|
|
22949
|
-
|
|
23044
|
+
types.push(type);
|
|
22950
23045
|
}
|
|
22951
|
-
return
|
|
23046
|
+
return types;
|
|
22952
23047
|
}
|
|
22953
23048
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
22954
|
-
function buildHaystack(
|
|
23049
|
+
function buildHaystack(t, extra) {
|
|
22955
23050
|
return [
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
22959
|
-
|
|
22960
|
-
|
|
22961
|
-
...g.instances.map((i) => i.repo),
|
|
22962
|
-
...g.instances.map((i) => i.file),
|
|
22963
|
-
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
22964
|
-
...g.instances.map((i) => i.id),
|
|
22965
|
-
// The people: the whole group's list when the store folded one, plus the
|
|
22966
|
-
// preview's own — the two overlap, and a haystack does not mind.
|
|
22967
|
-
...(g.users ?? []).map((u) => u.name),
|
|
22968
|
-
...g.instances.map((i) => i.user?.name ?? ""),
|
|
23051
|
+
t.subtype,
|
|
23052
|
+
t.category,
|
|
23053
|
+
t.policy.name,
|
|
23054
|
+
t.id,
|
|
23055
|
+
...(t.users ?? []).map((u) => u.name),
|
|
22969
23056
|
...extra === void 0 ? [] : [extra]
|
|
22970
23057
|
].join(" ").toLowerCase();
|
|
22971
23058
|
}
|
|
22972
|
-
function
|
|
22973
|
-
const cached2 = haystackCache.get(
|
|
23059
|
+
function typeHaystack(t) {
|
|
23060
|
+
const cached2 = haystackCache.get(t);
|
|
22974
23061
|
if (cached2 !== void 0) return cached2;
|
|
22975
|
-
const haystack = buildHaystack(
|
|
22976
|
-
haystackCache.set(
|
|
23062
|
+
const haystack = buildHaystack(t);
|
|
23063
|
+
haystackCache.set(t, haystack);
|
|
22977
23064
|
return haystack;
|
|
22978
23065
|
}
|
|
22979
23066
|
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
22980
|
-
function
|
|
22981
|
-
|
|
22982
|
-
if (cached2 !== void 0) return cached2;
|
|
22983
|
-
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
22984
|
-
actionsCache.set(g, actions);
|
|
22985
|
-
return actions;
|
|
23067
|
+
function typeActions(t) {
|
|
23068
|
+
return actionsCache.get(t) ?? [];
|
|
22986
23069
|
}
|
|
22987
23070
|
function countInstancesByStatus(statusInputs, statuses) {
|
|
22988
23071
|
const statusSet = new Set(statuses);
|
|
@@ -22993,8 +23076,8 @@ function countInstancesByStatus(statusInputs, statuses) {
|
|
|
22993
23076
|
}
|
|
22994
23077
|
return sum;
|
|
22995
23078
|
}
|
|
22996
|
-
function applyFindingFilters(
|
|
22997
|
-
let filtered =
|
|
23079
|
+
function applyFindingFilters(types, opts) {
|
|
23080
|
+
let filtered = types;
|
|
22998
23081
|
if (opts.severity && opts.severity.length > 0) {
|
|
22999
23082
|
const sevSet = new Set(opts.severity);
|
|
23000
23083
|
filtered = filtered.filter((g) => sevSet.has(g.severity));
|
|
@@ -23005,7 +23088,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
23005
23088
|
}
|
|
23006
23089
|
if (opts.actions && opts.actions.length > 0) {
|
|
23007
23090
|
const actionSet = new Set(opts.actions);
|
|
23008
|
-
filtered = filtered.filter((
|
|
23091
|
+
filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
|
|
23009
23092
|
}
|
|
23010
23093
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
23011
23094
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -23017,7 +23100,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
23017
23100
|
}
|
|
23018
23101
|
if (opts.q) {
|
|
23019
23102
|
const q = opts.q.toLowerCase();
|
|
23020
|
-
filtered = filtered.filter((
|
|
23103
|
+
filtered = filtered.filter((t) => typeHaystack(t).includes(q));
|
|
23021
23104
|
}
|
|
23022
23105
|
return filtered;
|
|
23023
23106
|
}
|
|
@@ -23032,11 +23115,11 @@ function compareFindingGroupOrder(a, b) {
|
|
|
23032
23115
|
if (recencyDiff !== 0) return recencyDiff;
|
|
23033
23116
|
return a.id.localeCompare(b.id);
|
|
23034
23117
|
}
|
|
23035
|
-
function
|
|
23036
|
-
return [...
|
|
23118
|
+
function sortFindingTypes(types) {
|
|
23119
|
+
return [...types].sort(compareFindingGroupOrder);
|
|
23037
23120
|
}
|
|
23038
|
-
function computeFindingFacets(
|
|
23039
|
-
const forSeverity = applyFindingFilters(
|
|
23121
|
+
function computeFindingFacets(allTypes, opts) {
|
|
23122
|
+
const forSeverity = applyFindingFilters(allTypes, {
|
|
23040
23123
|
providers: opts.providers,
|
|
23041
23124
|
actions: opts.actions,
|
|
23042
23125
|
statuses: opts.statuses,
|
|
@@ -23047,7 +23130,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23047
23130
|
for (const g of forSeverity) {
|
|
23048
23131
|
severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
|
|
23049
23132
|
}
|
|
23050
|
-
const forProvider = applyFindingFilters(
|
|
23133
|
+
const forProvider = applyFindingFilters(allTypes, {
|
|
23051
23134
|
actions: opts.actions,
|
|
23052
23135
|
statuses: opts.statuses,
|
|
23053
23136
|
q: opts.q,
|
|
@@ -23058,7 +23141,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23058
23141
|
for (const g of forProvider) {
|
|
23059
23142
|
for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
|
|
23060
23143
|
}
|
|
23061
|
-
const forAction = applyFindingFilters(
|
|
23144
|
+
const forAction = applyFindingFilters(allTypes, {
|
|
23062
23145
|
providers: opts.providers,
|
|
23063
23146
|
statuses: opts.statuses,
|
|
23064
23147
|
q: opts.q,
|
|
@@ -23067,9 +23150,9 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23067
23150
|
});
|
|
23068
23151
|
const actionMap = /* @__PURE__ */ new Map();
|
|
23069
23152
|
for (const g of forAction) {
|
|
23070
|
-
for (const a of
|
|
23153
|
+
for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
23071
23154
|
}
|
|
23072
|
-
const forSubtype = applyFindingFilters(
|
|
23155
|
+
const forSubtype = applyFindingFilters(allTypes, {
|
|
23073
23156
|
providers: opts.providers,
|
|
23074
23157
|
actions: opts.actions,
|
|
23075
23158
|
statuses: opts.statuses,
|
|
@@ -23078,7 +23161,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23078
23161
|
});
|
|
23079
23162
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
23080
23163
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
23081
|
-
const forStatus = applyFindingFilters(
|
|
23164
|
+
const forStatus = applyFindingFilters(allTypes, {
|
|
23082
23165
|
providers: opts.providers,
|
|
23083
23166
|
actions: opts.actions,
|
|
23084
23167
|
q: opts.q,
|
|
@@ -23126,10 +23209,20 @@ function matchesDimension(row, opts, dimension) {
|
|
|
23126
23209
|
return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
|
|
23127
23210
|
case "tools":
|
|
23128
23211
|
return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
|
|
23212
|
+
// An EMPTY value is a real filter here, not an absent one. The location
|
|
23213
|
+
// list buckets a finding whose event recorded no repo — or no file — under
|
|
23214
|
+
// the empty string, and selecting that bucket has to narrow the panel to
|
|
23215
|
+
// exactly it. Only `undefined` means "no filter"; a caller that wants every
|
|
23216
|
+
// row omits the key, which every call site already does.
|
|
23217
|
+
//
|
|
23218
|
+
// Reading '' as unset is what this replaced, and it failed in the one place
|
|
23219
|
+
// it mattered: the no-repo/no-file bucket is often the largest in a real
|
|
23220
|
+
// store, and its panel dropped both predicates and returned the WHOLE scope
|
|
23221
|
+
// — a row reading 3 findings beside a panel listing every finding there is.
|
|
23129
23222
|
case "repo":
|
|
23130
|
-
return opts.repo === void 0 ||
|
|
23223
|
+
return opts.repo === void 0 || row.repo === opts.repo;
|
|
23131
23224
|
case "file":
|
|
23132
|
-
return opts.file === void 0 ||
|
|
23225
|
+
return opts.file === void 0 || row.file === opts.file;
|
|
23133
23226
|
case "q":
|
|
23134
23227
|
return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
|
|
23135
23228
|
}
|
|
@@ -23243,6 +23336,23 @@ function addToLocation(acc, row) {
|
|
|
23243
23336
|
acc.statuses.push(row.status);
|
|
23244
23337
|
acc.ruleIds.add(row.ruleId);
|
|
23245
23338
|
}
|
|
23339
|
+
function compareLocationOrder(a, b) {
|
|
23340
|
+
const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
|
|
23341
|
+
const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
|
|
23342
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
23343
|
+
if (a.latestDetectedAt !== b.latestDetectedAt) {
|
|
23344
|
+
return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
|
|
23345
|
+
}
|
|
23346
|
+
if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
|
|
23347
|
+
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
23348
|
+
return 0;
|
|
23349
|
+
}
|
|
23350
|
+
function encodeLocationId(repo, file2) {
|
|
23351
|
+
return `${encodePart(repo)}/${encodePart(file2)}`;
|
|
23352
|
+
}
|
|
23353
|
+
function encodePart(value) {
|
|
23354
|
+
return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
|
|
23355
|
+
}
|
|
23246
23356
|
|
|
23247
23357
|
// ../../packages/schema/src/zod/installed-pack.ts
|
|
23248
23358
|
var InstalledPack = external_exports.object({
|
|
@@ -23751,11 +23861,11 @@ var WorkspaceSettings = external_exports.object({
|
|
|
23751
23861
|
// covers the current payload and must be re-granted.
|
|
23752
23862
|
modelJudgeConsent: ModelJudgeConsent.optional(),
|
|
23753
23863
|
// Records that the user consented to the DEFERRED send — the outbox — along
|
|
23754
|
-
// with the payload shape and the endpoint they agreed to. Since payload
|
|
23755
|
-
// that covers
|
|
23756
|
-
// carry prompt/reply text in `content
|
|
23757
|
-
// Absent until granted, and a grant for a different endpoint
|
|
23758
|
-
// payload no longer counts.
|
|
23864
|
+
// with the payload shape and the endpoint they agreed to. Since payload v3
|
|
23865
|
+
// that covers the pre-attach backlog AND undelivered captures alike, and both
|
|
23866
|
+
// carry prompt/reply/tool-output text in `content`; the key name predates
|
|
23867
|
+
// both widenings. Absent until granted, and a grant for a different endpoint
|
|
23868
|
+
// or an older payload no longer counts.
|
|
23759
23869
|
historySyncConsent: HistorySyncConsent.optional()
|
|
23760
23870
|
});
|
|
23761
23871
|
function defaultWorkspaceSettings() {
|
|
@@ -23886,6 +23996,9 @@ var ManagedSettingKey = external_exports.enum([
|
|
|
23886
23996
|
"dataSharesInPlace",
|
|
23887
23997
|
"redactFallback"
|
|
23888
23998
|
]).meta({ id: "ManagedSettingKey" });
|
|
23999
|
+
function isManagedSettingKey(value) {
|
|
24000
|
+
return ManagedSettingKey.safeParse(value).success;
|
|
24001
|
+
}
|
|
23889
24002
|
var ManagedSettingsValues = external_exports.object({
|
|
23890
24003
|
runMode: external_exports.enum(["standalone", "attached"]).optional(),
|
|
23891
24004
|
controlPlane: external_exports.object({
|
|
@@ -23910,7 +24023,27 @@ var ManagedSettings = external_exports.object({
|
|
|
23910
24023
|
// Which of those the user may not change. A key here with no matching value
|
|
23911
24024
|
// freezes whatever the user last chose; a value with no lock is a DEFAULT
|
|
23912
24025
|
// the user may still override. The two are separable on purpose.
|
|
23913
|
-
|
|
24026
|
+
//
|
|
24027
|
+
// Parsed as NAMES rather than as the enum, and split below: a name this
|
|
24028
|
+
// build does not know is dropped from the locked set and reported, never a
|
|
24029
|
+
// reason to refuse the file. The same shape reaches an older build whenever
|
|
24030
|
+
// an administrator locks a key a newer build added, and refusing it there
|
|
24031
|
+
// ran that build entirely unmanaged — every pin and lock gone — on exactly
|
|
24032
|
+
// the fleets most likely to carry a version skew. A name outside the enum
|
|
24033
|
+
// is still never HONOURED: the lockable set stays explicit above.
|
|
24034
|
+
lockedFields: external_exports.array(external_exports.string()).default([])
|
|
24035
|
+
}).transform(({ lockedFields, ...rest }) => {
|
|
24036
|
+
const known = [];
|
|
24037
|
+
const unknown2 = [];
|
|
24038
|
+
for (const name of lockedFields) {
|
|
24039
|
+
if (isManagedSettingKey(name)) known.push(name);
|
|
24040
|
+
else unknown2.push(name);
|
|
24041
|
+
}
|
|
24042
|
+
return {
|
|
24043
|
+
...rest,
|
|
24044
|
+
lockedFields: known,
|
|
24045
|
+
...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
|
|
24046
|
+
};
|
|
23914
24047
|
}).meta({ id: "ManagedSettings" });
|
|
23915
24048
|
|
|
23916
24049
|
// ../../packages/schema/src/zod/project-files.ts
|
|
@@ -24034,7 +24167,11 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
24034
24167
|
timestamp: external_exports.iso.date(),
|
|
24035
24168
|
critical: external_exports.number().int().nonnegative(),
|
|
24036
24169
|
high: external_exports.number().int().nonnegative(),
|
|
24037
|
-
medium: external_exports.number().int().nonnegative()
|
|
24170
|
+
medium: external_exports.number().int().nonnegative(),
|
|
24171
|
+
// Optional and additive, so a producer written against the earlier
|
|
24172
|
+
// three-series contract keeps validating. A consumer plotting it resolves the
|
|
24173
|
+
// absent case itself — the chart point requires a number.
|
|
24174
|
+
low: external_exports.number().int().nonnegative().optional()
|
|
24038
24175
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
24039
24176
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
24040
24177
|
range: TimeRange,
|
|
@@ -24060,6 +24197,10 @@ var ResolvedFeedItem = external_exports.object({
|
|
|
24060
24197
|
findingKey: external_exports.string(),
|
|
24061
24198
|
ruleId: external_exports.string(),
|
|
24062
24199
|
severity: Severity,
|
|
24200
|
+
// Repository slug, and the file path RELATIVE to it. The pair is what
|
|
24201
|
+
// identifies the file: a bare path matches the same name in every repo.
|
|
24202
|
+
// Optional and additive; empty when the event carried no repo.
|
|
24203
|
+
repo: external_exports.string().optional(),
|
|
24063
24204
|
path: external_exports.string(),
|
|
24064
24205
|
// ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
|
|
24065
24206
|
// findings domain). The reader `.toISOString()`s the DB epoch-ms values.
|
|
@@ -25603,7 +25744,8 @@ var SqliteActivityRepository = class {
|
|
|
25603
25744
|
SELECT 1 FROM audit_events d
|
|
25604
25745
|
WHERE d.root_session_id = audit_events.id
|
|
25605
25746
|
AND (d.content LIKE ? ESCAPE '\\'
|
|
25606
|
-
OR json_extract(d.attributes, '$.detail')
|
|
25747
|
+
OR coalesce(json_extract(d.attributes, '$.detail'),
|
|
25748
|
+
json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
|
|
25607
25749
|
);
|
|
25608
25750
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
25609
25751
|
}
|
|
@@ -26941,23 +27083,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
26941
27083
|
)`;
|
|
26942
27084
|
|
|
26943
27085
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
26944
|
-
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
26945
|
-
var DEFAULT_LOCATIONS_LIMIT = 100;
|
|
26946
|
-
var LOCATION_RULE_IDS_CAP = 20;
|
|
26947
|
-
function compareLocationOrder(a, b) {
|
|
26948
|
-
return compareFindingGroupOrder(
|
|
26949
|
-
{
|
|
26950
|
-
severity: a.maxSeverity,
|
|
26951
|
-
latestDetectedAt: a.latestDetectedAt,
|
|
26952
|
-
id: ""
|
|
26953
|
-
},
|
|
26954
|
-
{
|
|
26955
|
-
severity: b.maxSeverity,
|
|
26956
|
-
latestDetectedAt: b.latestDetectedAt,
|
|
26957
|
-
id: ""
|
|
26958
|
-
}
|
|
26959
|
-
);
|
|
26960
|
-
}
|
|
26961
27086
|
var CONCAT_SEP = ",";
|
|
26962
27087
|
var TUPLE_SEP = "|";
|
|
26963
27088
|
function splitConcat(value) {
|
|
@@ -27009,13 +27134,48 @@ function decodeGroupCursor(cursor) {
|
|
|
27009
27134
|
return null;
|
|
27010
27135
|
}
|
|
27011
27136
|
function firstAfter(sorted, cursor) {
|
|
27012
|
-
const index = sorted.findIndex((
|
|
27137
|
+
const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
|
|
27013
27138
|
return index === -1 ? sorted.length : index;
|
|
27014
27139
|
}
|
|
27015
27140
|
function findDeepLinked(sorted, page, id) {
|
|
27016
|
-
if (page.some((
|
|
27017
|
-
return sorted.find((
|
|
27141
|
+
if (page.some((t) => t.id === id)) return void 0;
|
|
27142
|
+
return sorted.find((t) => t.id === id);
|
|
27018
27143
|
}
|
|
27144
|
+
function encodeLocationCursor(location) {
|
|
27145
|
+
const payload = {
|
|
27146
|
+
sev: location.maxSeverity,
|
|
27147
|
+
t: location.latestDetectedAt,
|
|
27148
|
+
r: location.repo,
|
|
27149
|
+
f: location.file
|
|
27150
|
+
};
|
|
27151
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
27152
|
+
}
|
|
27153
|
+
function decodeLocationCursor(cursor) {
|
|
27154
|
+
const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
27155
|
+
if (parsed2 !== void 0 && typeof parsed2.sev === "string" && typeof parsed2.t === "string" && typeof parsed2.r === "string" && typeof parsed2.f === "string") {
|
|
27156
|
+
return { maxSeverity: parsed2.sev, latestDetectedAt: parsed2.t, repo: parsed2.r, file: parsed2.f };
|
|
27157
|
+
}
|
|
27158
|
+
return null;
|
|
27159
|
+
}
|
|
27160
|
+
function firstLocationAfter(sorted, cursor) {
|
|
27161
|
+
const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
|
|
27162
|
+
return index === -1 ? sorted.length : index;
|
|
27163
|
+
}
|
|
27164
|
+
function findDeepLinkedLocation(sorted, page, id) {
|
|
27165
|
+
if (page.some((l) => l.id === id)) return void 0;
|
|
27166
|
+
return sorted.find((l) => l.id === id);
|
|
27167
|
+
}
|
|
27168
|
+
var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
27169
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
27170
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
27171
|
+
e.started_at AS occurred_at,
|
|
27172
|
+
e.source_tool AS source_tool,
|
|
27173
|
+
e.repo AS repo,
|
|
27174
|
+
e.file_path AS file,
|
|
27175
|
+
e.tool_name AS tool_name,
|
|
27176
|
+
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
27177
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
27178
|
+
${latestResolutionStatusSql("f")} AS latest_status`;
|
|
27019
27179
|
var DAY_MS3 = 864e5;
|
|
27020
27180
|
var SqliteFindingsRepository = class {
|
|
27021
27181
|
constructor(db) {
|
|
@@ -27136,30 +27296,26 @@ var SqliteFindingsRepository = class {
|
|
|
27136
27296
|
);
|
|
27137
27297
|
}
|
|
27138
27298
|
/**
|
|
27139
|
-
*
|
|
27140
|
-
*
|
|
27141
|
-
*
|
|
27142
|
-
*
|
|
27143
|
-
*
|
|
27144
|
-
*
|
|
27145
|
-
*
|
|
27146
|
-
*
|
|
27147
|
-
*
|
|
27148
|
-
*
|
|
27149
|
-
*
|
|
27150
|
-
*
|
|
27299
|
+
* Finding TYPES for the dashboard — one row per rule, scoped to the four
|
|
27300
|
+
* capture kinds (audit_events also holds structural/reconciler/scan rows this
|
|
27301
|
+
* list must never surface), with per-filter-excluded facets, the requested
|
|
27302
|
+
* filters applied, and sorted by severity then recency. Filtering and faceting
|
|
27303
|
+
* run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
|
|
27304
|
+
* full filtered set; `items` is the requested page (default 50), keyset-paged.
|
|
27305
|
+
* Under a `status` filter, `totals.findings` counts only findings whose
|
|
27306
|
+
* derived status was requested.
|
|
27307
|
+
*
|
|
27308
|
+
* ONE read, which materializes no findings: a single aggregate per rule_id,
|
|
27309
|
+
* folding EVERY finding into the numbers a type row and the filters need
|
|
27310
|
+
* (count, severity, category, providers, actions, statuses, latest, search
|
|
27311
|
+
* text). The findings OF a type come from listFindingInstances scoped to
|
|
27312
|
+
* `subtype`, so neither list bounds the other and no per-type cap exists.
|
|
27151
27313
|
*
|
|
27152
|
-
* Two reads, neither of which materializes a row per finding:
|
|
27153
|
-
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
27154
|
-
* the group and the filters need (count, providers, actions, statuses,
|
|
27155
|
-
* latest, search text);
|
|
27156
|
-
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
27157
|
-
* populate `instances` for the table's expanded rows.
|
|
27158
27314
|
* The aggregates carry raw DB values and are translated by the same
|
|
27159
|
-
* @akasecurity/schema mappers
|
|
27160
|
-
* rule is ever restated in SQL.
|
|
27315
|
+
* @akasecurity/schema mappers every other path uses, so no enum mapping or
|
|
27316
|
+
* status rule is ever restated in SQL.
|
|
27161
27317
|
*/
|
|
27162
|
-
|
|
27318
|
+
listFindingTypes(query) {
|
|
27163
27319
|
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
27164
27320
|
const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
|
|
27165
27321
|
const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
|
|
@@ -27172,12 +27328,7 @@ var SqliteFindingsRepository = class {
|
|
|
27172
27328
|
predicate,
|
|
27173
27329
|
params: sessionParams
|
|
27174
27330
|
});
|
|
27175
|
-
const
|
|
27176
|
-
sessionId: query.sessionId,
|
|
27177
|
-
from: query.from
|
|
27178
|
-
});
|
|
27179
|
-
const groupable = rows.map(toFlatFindingRow);
|
|
27180
|
-
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
27331
|
+
const allTypes = buildFindingTypes(aggregates);
|
|
27181
27332
|
const filterOpts = {
|
|
27182
27333
|
severity: query.severity,
|
|
27183
27334
|
providers: query.provider,
|
|
@@ -27186,30 +27337,25 @@ var SqliteFindingsRepository = class {
|
|
|
27186
27337
|
subtype: query.subtype,
|
|
27187
27338
|
q: query.q
|
|
27188
27339
|
};
|
|
27189
|
-
const facets = computeFindingFacets(
|
|
27190
|
-
const sorted =
|
|
27340
|
+
const facets = computeFindingFacets(allTypes, filterOpts);
|
|
27341
|
+
const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
|
|
27191
27342
|
const statusFilter = query.status ?? [];
|
|
27192
27343
|
const totals = {
|
|
27193
|
-
findings: sorted.reduce((acc,
|
|
27194
|
-
if (statusFilter.length === 0) return acc +
|
|
27195
|
-
const agg = aggregates.get(
|
|
27196
|
-
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ??
|
|
27344
|
+
findings: sorted.reduce((acc, t) => {
|
|
27345
|
+
if (statusFilter.length === 0) return acc + t.instanceCount;
|
|
27346
|
+
const agg = aggregates.get(t.id);
|
|
27347
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
|
|
27197
27348
|
}, 0),
|
|
27198
|
-
|
|
27349
|
+
types: sorted.length
|
|
27199
27350
|
};
|
|
27200
|
-
const limit = query.limit ??
|
|
27351
|
+
const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
|
|
27201
27352
|
const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
|
|
27202
27353
|
const start = cursor === null ? 0 : firstAfter(sorted, cursor);
|
|
27203
27354
|
const page = sorted.slice(start, start + limit);
|
|
27204
27355
|
const lastOnPage = page.at(-1);
|
|
27205
27356
|
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
|
|
27206
27357
|
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
|
|
27207
|
-
const
|
|
27208
|
-
const narrow = (g) => statusSet ? {
|
|
27209
|
-
...g,
|
|
27210
|
-
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
27211
|
-
} : g;
|
|
27212
|
-
const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
|
|
27358
|
+
const items = [...page, ...deepLinked ? [deepLinked] : []];
|
|
27213
27359
|
return Promise.resolve({
|
|
27214
27360
|
totals,
|
|
27215
27361
|
facets,
|
|
@@ -27220,7 +27366,7 @@ var SqliteFindingsRepository = class {
|
|
|
27220
27366
|
}
|
|
27221
27367
|
/**
|
|
27222
27368
|
* One row per rule_id, folding EVERY instance of the group into the values
|
|
27223
|
-
*
|
|
27369
|
+
* buildFindingTypes cannot recover from an aggregate. Bounded by the number of
|
|
27224
27370
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
27225
27371
|
*
|
|
27226
27372
|
* A single scan, folded in two levels: the inner SELECT groups by
|
|
@@ -27314,13 +27460,25 @@ var SqliteFindingsRepository = class {
|
|
|
27314
27460
|
});
|
|
27315
27461
|
}
|
|
27316
27462
|
/**
|
|
27317
|
-
* The same findings folded by
|
|
27463
|
+
* The same findings folded by WHERE they live — one row per (repo, file) pair.
|
|
27318
27464
|
*
|
|
27319
27465
|
* The grouping keys come from the capturing event's attributes, which is what
|
|
27320
|
-
* the local store relates a finding to
|
|
27321
|
-
*
|
|
27322
|
-
* empty-string bucket, which
|
|
27323
|
-
*
|
|
27466
|
+
* the local store relates a finding to; there is no finding↔asset row to group
|
|
27467
|
+
* by instead. A repo or file the event did not record folds into the
|
|
27468
|
+
* empty-string bucket, which is a real location like any other: it is listed,
|
|
27469
|
+
* it is selectable, and its `?loc=` token is as good as any other row's.
|
|
27470
|
+
*
|
|
27471
|
+
* ONE flat list rather than repos nesting files. A rollup can only be paged by
|
|
27472
|
+
* repo, which leaves the file list inside it unbounded — the shape the by-type
|
|
27473
|
+
* list was rebuilt to remove — and two-level pagination inside an
|
|
27474
|
+
* expand/collapse table is what pushed that view to master/detail in the first
|
|
27475
|
+
* place.
|
|
27476
|
+
*
|
|
27477
|
+
* Every filter narrows the FINDINGS and the locations fall out of what
|
|
27478
|
+
* survives, so each row's `instanceCount` is exactly what listFindingInstances
|
|
27479
|
+
* reports for the same filters scoped to that pair. The view depends on it:
|
|
27480
|
+
* one toolbar sits over both panels precisely because a location owns none of
|
|
27481
|
+
* its fields.
|
|
27324
27482
|
*/
|
|
27325
27483
|
listFindingLocations(query) {
|
|
27326
27484
|
const opts = {
|
|
@@ -27332,13 +27490,16 @@ var SqliteFindingsRepository = class {
|
|
|
27332
27490
|
tools: query.tool,
|
|
27333
27491
|
q: query.q
|
|
27334
27492
|
};
|
|
27335
|
-
const limit = query.limit ??
|
|
27493
|
+
const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
|
|
27494
|
+
const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
|
|
27336
27495
|
const byRepo = /* @__PURE__ */ new Map();
|
|
27496
|
+
const accumulator = createInstanceFacetAccumulator(opts);
|
|
27337
27497
|
let total = 0;
|
|
27338
27498
|
for (const row of this.scanFindingRows({
|
|
27339
27499
|
sessionId: query.sessionId,
|
|
27340
27500
|
from: query.from
|
|
27341
27501
|
})) {
|
|
27502
|
+
accumulator.add(row);
|
|
27342
27503
|
if (!matchesInstanceFilters(row, opts)) continue;
|
|
27343
27504
|
total += 1;
|
|
27344
27505
|
let files = byRepo.get(row.repo);
|
|
@@ -27353,103 +27514,35 @@ var SqliteFindingsRepository = class {
|
|
|
27353
27514
|
}
|
|
27354
27515
|
addToLocation(acc, row);
|
|
27355
27516
|
}
|
|
27356
|
-
|
|
27357
|
-
const
|
|
27358
|
-
|
|
27359
|
-
|
|
27360
|
-
|
|
27361
|
-
|
|
27362
|
-
|
|
27363
|
-
|
|
27364
|
-
|
|
27365
|
-
|
|
27366
|
-
|
|
27367
|
-
|
|
27368
|
-
|
|
27369
|
-
|
|
27370
|
-
|
|
27371
|
-
|
|
27372
|
-
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
|
|
27376
|
-
|
|
27377
|
-
|
|
27378
|
-
);
|
|
27379
|
-
const statuses = fileRows.map((f) => f.status);
|
|
27380
|
-
const folded = foldGroupStatus(statuses);
|
|
27381
|
-
return {
|
|
27382
|
-
repo,
|
|
27383
|
-
instanceCount: rollup.instanceCount,
|
|
27384
|
-
maxSeverity: rollup.maxSeverity,
|
|
27385
|
-
latestDetectedAt: rollup.latestDetectedAt,
|
|
27386
|
-
...folded === void 0 ? {} : { status: folded },
|
|
27387
|
-
files: fileRows
|
|
27388
|
-
};
|
|
27389
|
-
});
|
|
27390
|
-
repos.sort(compareLocationOrder);
|
|
27517
|
+
const sorted = [];
|
|
27518
|
+
for (const [repo, files] of byRepo) {
|
|
27519
|
+
for (const [file2, acc] of files) {
|
|
27520
|
+
const status = foldGroupStatus(acc.statuses);
|
|
27521
|
+
sorted.push({
|
|
27522
|
+
id: encodeLocationId(repo, file2),
|
|
27523
|
+
repo,
|
|
27524
|
+
file: file2,
|
|
27525
|
+
instanceCount: acc.instanceCount,
|
|
27526
|
+
maxSeverity: acc.maxSeverity,
|
|
27527
|
+
latestDetectedAt: acc.latestDetectedAt,
|
|
27528
|
+
...status === void 0 ? {} : { status },
|
|
27529
|
+
ruleIds: [...acc.ruleIds]
|
|
27530
|
+
});
|
|
27531
|
+
}
|
|
27532
|
+
}
|
|
27533
|
+
sorted.sort(compareLocationOrder);
|
|
27534
|
+
const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
|
|
27535
|
+
const page = sorted.slice(start, start + limit);
|
|
27536
|
+
const lastOnPage = page.at(-1);
|
|
27537
|
+
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
|
|
27538
|
+
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
|
|
27391
27539
|
return Promise.resolve({
|
|
27392
|
-
totals: { findings: total,
|
|
27393
|
-
|
|
27394
|
-
|
|
27540
|
+
totals: { findings: total, locations: sorted.length },
|
|
27541
|
+
facets: accumulator.facets(),
|
|
27542
|
+
items: [...page, ...deepLinked ? [deepLinked] : []],
|
|
27543
|
+
nextCursor
|
|
27395
27544
|
});
|
|
27396
27545
|
}
|
|
27397
|
-
/**
|
|
27398
|
-
* Each group's newest instances, for the table's expanded rows.
|
|
27399
|
-
*
|
|
27400
|
-
* ONE index-ordered scan with early termination, and the shape is the point.
|
|
27401
|
-
* The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
|
|
27402
|
-
* started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
|
|
27403
|
-
* through a temp B-tree to keep a bounded preview of each group, and then
|
|
27404
|
-
* sorts the survivors again for the page order. Both sorts grow with the
|
|
27405
|
-
* store while the answer does not.
|
|
27406
|
-
*
|
|
27407
|
-
* Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
|
|
27408
|
-
* (or the session or window index the scope names — see `findingScanSql`),
|
|
27409
|
-
* which is already the order the page wants, and keeps rows per rule until
|
|
27410
|
-
* each rule has as many as it can show. The aggregate the caller already holds
|
|
27411
|
-
* says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
|
|
27412
|
-
* per rule, summed, is the number of rows this scan has to find, and it stops
|
|
27413
|
-
* on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
|
|
27414
|
-
* (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
|
|
27415
|
-
* store with many firing rules widens it. The bound that DOES hold
|
|
27416
|
-
* unconditionally is the sorted form's floor: this scan visits at most as
|
|
27417
|
-
* many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
|
|
27418
|
-
* sorted, and stops the moment every rule has its cap, where the sorted form
|
|
27419
|
-
* sorts the whole scope regardless. The true worst case — the rarest rule's
|
|
27420
|
-
* wanted instances sitting at the tail of the scope — is one pass over
|
|
27421
|
-
* everything in scope with a block sort of the id tie-break only, never a
|
|
27422
|
-
* sort of the scope, which is still that floor.
|
|
27423
|
-
*
|
|
27424
|
-
* A row whose rule the aggregate did not see is skipped: the two statements
|
|
27425
|
-
* run without a shared snapshot, so a capture landing between them can add a
|
|
27426
|
-
* rule here that has no counts there, and the counts are what the group is
|
|
27427
|
-
* built from.
|
|
27428
|
-
*/
|
|
27429
|
-
previewRows(aggregates, scope) {
|
|
27430
|
-
const wanted = /* @__PURE__ */ new Map();
|
|
27431
|
-
let remaining = 0;
|
|
27432
|
-
for (const [ruleId, agg] of aggregates) {
|
|
27433
|
-
const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
|
|
27434
|
-
wanted.set(ruleId, n);
|
|
27435
|
-
remaining += n;
|
|
27436
|
-
}
|
|
27437
|
-
const rows = [];
|
|
27438
|
-
if (remaining === 0) return rows;
|
|
27439
|
-
const { sql, params } = this.findingScanSql(scope);
|
|
27440
|
-
const taken = /* @__PURE__ */ new Map();
|
|
27441
|
-
for (const r of iterateRows(this.db.prepare(sql), params)) {
|
|
27442
|
-
const want = wanted.get(r.rule_id);
|
|
27443
|
-
if (want === void 0) continue;
|
|
27444
|
-
const have = taken.get(r.rule_id) ?? 0;
|
|
27445
|
-
if (have >= want) continue;
|
|
27446
|
-
taken.set(r.rule_id, have + 1);
|
|
27447
|
-
rows.push(r);
|
|
27448
|
-
remaining -= 1;
|
|
27449
|
-
if (remaining === 0) break;
|
|
27450
|
-
}
|
|
27451
|
-
return rows;
|
|
27452
|
-
}
|
|
27453
27546
|
/**
|
|
27454
27547
|
* Every finding in scope as a FlatFindingRow, newest first, streamed.
|
|
27455
27548
|
*
|
|
@@ -27476,6 +27569,33 @@ var SqliteFindingsRepository = class {
|
|
|
27476
27569
|
yield toFlatFindingRow(r);
|
|
27477
27570
|
}
|
|
27478
27571
|
}
|
|
27572
|
+
/**
|
|
27573
|
+
* One finding by its own id, or null when no such row exists.
|
|
27574
|
+
*
|
|
27575
|
+
* A primary-key seek on `inspection_findings`, so its cost does not grow with
|
|
27576
|
+
* the store — and, unlike anything derived from a list page, it resolves a
|
|
27577
|
+
* finding of ANY age. That is what the Findings page's one-shot `?finding=`
|
|
27578
|
+
* deep link needs: the id it carries may name a finding thousands of rows
|
|
27579
|
+
* older than anything a first page holds.
|
|
27580
|
+
*
|
|
27581
|
+
* Deliberately UNFILTERED — no capture-kind, session or time predicate. It
|
|
27582
|
+
* RESOLVES an id; whether that row would survive the list's current filters is
|
|
27583
|
+
* a different question, and hiding the target because a filter excludes it is
|
|
27584
|
+
* worse than showing it.
|
|
27585
|
+
*
|
|
27586
|
+
* `groupId` on the result IS the rule id, so this one read answers both "which
|
|
27587
|
+
* type should the list select?" and "what does the drawer show?".
|
|
27588
|
+
*/
|
|
27589
|
+
findingInstance(id) {
|
|
27590
|
+
const row = this.db.prepare(
|
|
27591
|
+
`SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27592
|
+
FROM inspection_findings f
|
|
27593
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
27594
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27595
|
+
WHERE f.id = ?`
|
|
27596
|
+
).get(id);
|
|
27597
|
+
return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
|
|
27598
|
+
}
|
|
27479
27599
|
/**
|
|
27480
27600
|
* The one statement both instance-level scans run: every finding in scope,
|
|
27481
27601
|
* joined to its event and definition, newest first.
|
|
@@ -27509,17 +27629,7 @@ var SqliteFindingsRepository = class {
|
|
|
27509
27629
|
conditions.push("e.started_at >= ?");
|
|
27510
27630
|
params.push(isoToEpochMillis(scope.from));
|
|
27511
27631
|
}
|
|
27512
|
-
const sql = `SELECT
|
|
27513
|
-
d.severity AS severity, f.masked_match AS masked_match,
|
|
27514
|
-
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
27515
|
-
e.started_at AS occurred_at,
|
|
27516
|
-
e.source_tool AS source_tool,
|
|
27517
|
-
e.repo AS repo,
|
|
27518
|
-
e.file_path AS file,
|
|
27519
|
-
e.tool_name AS tool_name,
|
|
27520
|
-
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
27521
|
-
e.event_type AS kind, f.finding_key AS finding_key,
|
|
27522
|
-
${latestResolutionStatusSql("f")} AS latest_status
|
|
27632
|
+
const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27523
27633
|
FROM audit_events e
|
|
27524
27634
|
CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
|
|
27525
27635
|
CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -27533,6 +27643,26 @@ var SqliteFindingsRepository = class {
|
|
|
27533
27643
|
group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
27534
27644
|
const rows = this.db.prepare(
|
|
27535
27645
|
`SELECT rule_id,
|
|
27646
|
+
-- BARE columns beside max(latest_at), which is deliberate and
|
|
27647
|
+
-- is SQLite's documented behaviour: with a single min()/max()
|
|
27648
|
+
-- in an aggregate query, every bare column takes its value from
|
|
27649
|
+
-- the row that produced the extremum. So these are the severity
|
|
27650
|
+
-- and category of the definition whose finding is NEWEST, which
|
|
27651
|
+
-- is what the row-based build they replaced read off its first
|
|
27652
|
+
-- (newest-first) row.
|
|
27653
|
+
--
|
|
27654
|
+
-- min() is WRONG here and was the defect: inspection_definitions
|
|
27655
|
+
-- holds one row per rule VERSION (see its writer \u2014 a version bump
|
|
27656
|
+
-- mints a new row), so a rule whose severity moved between
|
|
27657
|
+
-- versions has several, and min() picks the ALPHABETICALLY
|
|
27658
|
+
-- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
|
|
27659
|
+
-- That is arbitrary in direction, and it feeds the badge, the
|
|
27660
|
+
-- filter, the facet counts and the primary sort key.
|
|
27661
|
+
--
|
|
27662
|
+
-- Adding a second min()/max() aggregate here would make these
|
|
27663
|
+
-- bare columns ambiguous again; keep max(latest_at) the only one.
|
|
27664
|
+
severity,
|
|
27665
|
+
category,
|
|
27536
27666
|
sum(tuple_count) AS instance_count,
|
|
27537
27667
|
max(latest_at) AS latest_at,
|
|
27538
27668
|
group_concat(source_tools) AS source_tools,
|
|
@@ -27543,6 +27673,14 @@ var SqliteFindingsRepository = class {
|
|
|
27543
27673
|
group_concat(tool_names) AS tool_names
|
|
27544
27674
|
FROM (
|
|
27545
27675
|
SELECT d.rule_id AS rule_id,
|
|
27676
|
+
-- Severity and category are columns of the DEFINITION, and
|
|
27677
|
+
-- a rule can have SEVERAL definitions (one per version), so
|
|
27678
|
+
-- these are grouped on below and resolved to the newest
|
|
27679
|
+
-- firing version by the outer query's bare-column select.
|
|
27680
|
+
-- They ride the aggregate because the type build has no rows
|
|
27681
|
+
-- to read them off \u2014 see buildFindingTypes.
|
|
27682
|
+
d.severity AS severity,
|
|
27683
|
+
d.category AS category,
|
|
27546
27684
|
e.event_type || '${TUPLE_SEP}' ||
|
|
27547
27685
|
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
27548
27686
|
coalesce(latest.status, '') AS status_tuple,
|
|
@@ -27557,7 +27695,7 @@ var SqliteFindingsRepository = class {
|
|
|
27557
27695
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
27558
27696
|
ON latest.finding_key = f.finding_key
|
|
27559
27697
|
${scope.predicate}
|
|
27560
|
-
GROUP BY d.rule_id, status_tuple
|
|
27698
|
+
GROUP BY d.rule_id, d.severity, d.category, status_tuple
|
|
27561
27699
|
)
|
|
27562
27700
|
GROUP BY rule_id`
|
|
27563
27701
|
).all(scope.params);
|
|
@@ -27566,6 +27704,8 @@ var SqliteFindingsRepository = class {
|
|
|
27566
27704
|
r.rule_id,
|
|
27567
27705
|
{
|
|
27568
27706
|
instanceCount: r.instance_count,
|
|
27707
|
+
severity: r.severity,
|
|
27708
|
+
category: r.category,
|
|
27569
27709
|
sourceTools: splitConcat(r.source_tools),
|
|
27570
27710
|
actionsTaken: splitConcat(r.actions_taken),
|
|
27571
27711
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
@@ -27582,7 +27722,7 @@ var SqliteFindingsRepository = class {
|
|
|
27582
27722
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
27583
27723
|
// Free text only — joined and substring-matched, so group_concat's
|
|
27584
27724
|
// commas need no unpicking (a repo/path containing one still matches).
|
|
27585
|
-
// Left undefined (not '') when unfetched, so
|
|
27725
|
+
// Left undefined (not '') when unfetched, so buildFindingTypes can
|
|
27586
27726
|
// tell "no q this request" from "a group with no repo/file at all"
|
|
27587
27727
|
// and skip priming a haystack nothing will read.
|
|
27588
27728
|
...withSearchText ? {
|
|
@@ -27734,6 +27874,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27734
27874
|
this.markOwedStmt = db.prepare(
|
|
27735
27875
|
`UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
|
|
27736
27876
|
);
|
|
27877
|
+
this.markCaptureBacklogOwedStmt = db.prepare(
|
|
27878
|
+
`UPDATE audit_events SET outbox_owed = 1
|
|
27879
|
+
WHERE synced_at IS NULL
|
|
27880
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27881
|
+
AND started_at < :before`
|
|
27882
|
+
);
|
|
27737
27883
|
this.stampStmt = db.prepare(
|
|
27738
27884
|
`UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
|
|
27739
27885
|
);
|
|
@@ -27782,7 +27928,9 @@ var SqliteHistorySyncRepository = class {
|
|
|
27782
27928
|
);
|
|
27783
27929
|
this.disownCapturesStmt = db.prepare(
|
|
27784
27930
|
`UPDATE audit_events SET outbox_owed = NULL
|
|
27785
|
-
WHERE outbox_owed IS NOT NULL
|
|
27931
|
+
WHERE outbox_owed IS NOT NULL
|
|
27932
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27933
|
+
AND started_at < :attachedAt`
|
|
27786
27934
|
);
|
|
27787
27935
|
this.rearmStmt = db.prepare(
|
|
27788
27936
|
`UPDATE audit_events SET synced_at = NULL
|
|
@@ -27858,6 +28006,7 @@ var SqliteHistorySyncRepository = class {
|
|
|
27858
28006
|
freezeBoundaryStmt;
|
|
27859
28007
|
captureRowsStmt;
|
|
27860
28008
|
markOwedStmt;
|
|
28009
|
+
markCaptureBacklogOwedStmt;
|
|
27861
28010
|
captureSkipCountStmt;
|
|
27862
28011
|
disownCapturesStmt;
|
|
27863
28012
|
partitionStmt;
|
|
@@ -27921,6 +28070,23 @@ var SqliteHistorySyncRepository = class {
|
|
|
27921
28070
|
markCaptureOwed(id) {
|
|
27922
28071
|
this.markOwedStmt.run({ id });
|
|
27923
28072
|
}
|
|
28073
|
+
/**
|
|
28074
|
+
* Mark every capture already on disk as owed, as of `before`.
|
|
28075
|
+
*
|
|
28076
|
+
* The consent-time backfill, called once from `aka attach` when a human
|
|
28077
|
+
* grants existing-history consent — never from an ongoing drain pass, and
|
|
28078
|
+
* never inferred from a boundary that could later move. `before` is the
|
|
28079
|
+
* caller's own "now" at the moment consent was granted, so what this marks
|
|
28080
|
+
* is exactly the backlog the consent prompt already counted, not whatever a
|
|
28081
|
+
* later re-attach or key rotation might widen it to.
|
|
28082
|
+
*
|
|
28083
|
+
* Returns how many rows matched, for the caller to log or test against. Not a
|
|
28084
|
+
* count of NEWLY marked rows — a row still unsynced from an earlier call
|
|
28085
|
+
* matches again and is counted again, the same as `UPDATE`'s own `changes`.
|
|
28086
|
+
*/
|
|
28087
|
+
markCaptureBacklogOwed(before) {
|
|
28088
|
+
return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
|
|
28089
|
+
}
|
|
27924
28090
|
/** Record delivery. Called only AFTER the far side has accepted the rows. */
|
|
27925
28091
|
markSynced(ids, atMs) {
|
|
27926
28092
|
this.stampAll(ids, atMs);
|
|
@@ -28037,15 +28203,42 @@ var SqliteHistorySyncRepository = class {
|
|
|
28037
28203
|
*
|
|
28038
28204
|
* Delivery is a fact about ONE recipient: rows sent to the deployment a
|
|
28039
28205
|
* machine has just left are undelivered as far as the new one is concerned.
|
|
28040
|
-
* All
|
|
28041
|
-
* attributed to the wrong deployment,
|
|
28206
|
+
* All four in one transaction, so a crash between them cannot leave stamps
|
|
28207
|
+
* attributed to the wrong deployment, a boundary that belongs to another, or
|
|
28208
|
+
* a disown with no re-mark to follow it.
|
|
28042
28209
|
*
|
|
28043
28210
|
* The boundary is written HERE and only here, which is what freezes it: a
|
|
28044
28211
|
* re-attach to the SAME deployment (a key rotation) leaves the fingerprint
|
|
28045
28212
|
* unchanged, so this never runs and the backlog does not widen back over rows
|
|
28046
28213
|
* the live path has since delivered.
|
|
28214
|
+
*
|
|
28215
|
+
* `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
|
|
28216
|
+
* granted existing-history consent for the deployment this call is arming —
|
|
28217
|
+
* a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
|
|
28218
|
+
* instant, `backlogBefore` is the ATTACH instant, and the two can be far
|
|
28219
|
+
* apart. Passed only when that grant is valid, since this method has no way
|
|
28220
|
+
* to check consent itself and must not mark a row owed for a machine that
|
|
28221
|
+
* never agreed to it. Applied AFTER the disown above, in the SAME
|
|
28222
|
+
* transaction: what the disown clears is every marker below `backlogBefore`,
|
|
28223
|
+
* which includes this deployment's OWN pre-attach rows — `aka attach` calls
|
|
28224
|
+
* `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
|
|
28225
|
+
* on the cleared side of that bound — and the re-mark in the same
|
|
28226
|
+
* transaction is what puts those rows back. A crash between the two cannot
|
|
28227
|
+
* strand the ledger disowned with nothing re-marked — the transaction either
|
|
28228
|
+
* lands whole or not at all, and a fingerprint mismatch that has not yet
|
|
28229
|
+
* committed re-enters this method on the very next pass. Omit it (the
|
|
28230
|
+
* structural-only tests do) to exercise the disown in isolation.
|
|
28231
|
+
*
|
|
28232
|
+
* The disown is bounded by `backlogBefore`, which is what keeps it from
|
|
28233
|
+
* touching a marker the NEW deployment's OWN live path has already set: B's
|
|
28234
|
+
* live path can mark a capture owed from the moment `aka attach` writes the
|
|
28235
|
+
* descriptor, before the drain's first pass ever reaches this method, and
|
|
28236
|
+
* such a row sits at or after the bound rather than below it. What keeps the
|
|
28237
|
+
* disown from eating THIS SAME CALL's own re-mark is the order, not the
|
|
28238
|
+
* bound — disown runs first, re-mark second, both inside the one
|
|
28239
|
+
* transaction above.
|
|
28047
28240
|
*/
|
|
28048
|
-
rearmFor(fingerprint, backlogBefore) {
|
|
28241
|
+
rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
|
|
28049
28242
|
this.ensureRowStmt.run();
|
|
28050
28243
|
withTransaction(
|
|
28051
28244
|
this.db,
|
|
@@ -28053,7 +28246,10 @@ var SqliteHistorySyncRepository = class {
|
|
|
28053
28246
|
const previous = getRow(this.fingerprintStmt)?.fingerprint;
|
|
28054
28247
|
this.rearmStmt.run();
|
|
28055
28248
|
if (previous !== null && previous !== void 0 && previous !== fingerprint) {
|
|
28056
|
-
this.disownCapturesStmt.run();
|
|
28249
|
+
this.disownCapturesStmt.run({ attachedAt: backlogBefore });
|
|
28250
|
+
}
|
|
28251
|
+
if (backfillCapturesBefore !== void 0) {
|
|
28252
|
+
this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
|
|
28057
28253
|
}
|
|
28058
28254
|
this.setFingerprintStmt.run({ fingerprint, backlogBefore });
|
|
28059
28255
|
},
|
|
@@ -28330,7 +28526,8 @@ function managedSettingsPaths(platform2 = process.platform) {
|
|
|
28330
28526
|
}
|
|
28331
28527
|
return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
|
|
28332
28528
|
}
|
|
28333
|
-
|
|
28529
|
+
var testOnlyManagedPaths = null;
|
|
28530
|
+
function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
|
|
28334
28531
|
for (const path of paths) {
|
|
28335
28532
|
let text;
|
|
28336
28533
|
try {
|
|
@@ -30810,7 +31007,7 @@ function toUtcDateString(ms) {
|
|
|
30810
31007
|
return new Date(ms).toISOString().slice(0, 10);
|
|
30811
31008
|
}
|
|
30812
31009
|
function isTimeseriesSeverity(s) {
|
|
30813
|
-
return s === "critical" || s === "high" || s === "medium";
|
|
31010
|
+
return s === "critical" || s === "high" || s === "medium" || s === "low";
|
|
30814
31011
|
}
|
|
30815
31012
|
var SqliteSecurityRepository = class {
|
|
30816
31013
|
constructor(db, now = () => Date.now()) {
|
|
@@ -30939,12 +31136,16 @@ var SqliteSecurityRepository = class {
|
|
|
30939
31136
|
const now = this.now();
|
|
30940
31137
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
30941
31138
|
const rows = this.findingsInRange(windowStart, now);
|
|
30942
|
-
const points = Array.from(
|
|
30943
|
-
|
|
30944
|
-
|
|
30945
|
-
|
|
30946
|
-
|
|
30947
|
-
|
|
31139
|
+
const points = Array.from(
|
|
31140
|
+
{ length: numBuckets },
|
|
31141
|
+
(_, i) => ({
|
|
31142
|
+
timestamp: toUtcDateString(windowStart + i * bucketMs),
|
|
31143
|
+
critical: 0,
|
|
31144
|
+
high: 0,
|
|
31145
|
+
medium: 0,
|
|
31146
|
+
low: 0
|
|
31147
|
+
})
|
|
31148
|
+
);
|
|
30948
31149
|
for (const r of rows) {
|
|
30949
31150
|
const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
|
|
30950
31151
|
const bucket = points[idx];
|
|
@@ -31162,6 +31363,7 @@ var SqliteSecurityRepository = class {
|
|
|
31162
31363
|
`SELECT f.finding_key AS finding_key,
|
|
31163
31364
|
d.rule_id AS rule_id,
|
|
31164
31365
|
d.severity AS severity,
|
|
31366
|
+
e.repo AS repo,
|
|
31165
31367
|
e.file_path AS path,
|
|
31166
31368
|
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
31167
31369
|
latest.resolved_at AS latest_resolved_at
|
|
@@ -31181,6 +31383,7 @@ var SqliteSecurityRepository = class {
|
|
|
31181
31383
|
const items = rows.map((r) => ({
|
|
31182
31384
|
findingKey: r.finding_key,
|
|
31183
31385
|
ruleId: r.rule_id,
|
|
31386
|
+
repo: r.repo ?? "",
|
|
31184
31387
|
severity: r.severity,
|
|
31185
31388
|
path: r.path ?? "",
|
|
31186
31389
|
resolvedAt: new Date(r.latest_resolved_at).toISOString(),
|
|
@@ -31190,13 +31393,66 @@ var SqliteSecurityRepository = class {
|
|
|
31190
31393
|
}));
|
|
31191
31394
|
return Promise.resolve({ items });
|
|
31192
31395
|
}
|
|
31396
|
+
/**
|
|
31397
|
+
* Per-rule tallies of the findings that are still OPEN, whole-store.
|
|
31398
|
+
*
|
|
31399
|
+
* Scoped by status rather than by time, because the card this feeds is a to-do
|
|
31400
|
+
* list: a secret committed three weeks ago and never rotated is still the most
|
|
31401
|
+
* important thing to fix, and any window hides it. It carried a "newest N
|
|
31402
|
+
* findings" cap and then a range; the first meant a different span on every
|
|
31403
|
+
* machine, and the second reported "no recommendations" over live exposure.
|
|
31404
|
+
*
|
|
31405
|
+
* `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
|
|
31406
|
+
* so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
|
|
31407
|
+
* is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
|
|
31408
|
+
* dismissal is a judgement, not a remediation) and drops untracked legacy rows.
|
|
31409
|
+
* The two answer different questions and only this one has to match a link.
|
|
31410
|
+
*
|
|
31411
|
+
* Aggregated in SQL: the result is O(distinct rule × category × severity), so a
|
|
31412
|
+
* whole-store scope costs a grouped scan rather than a row per finding.
|
|
31413
|
+
*/
|
|
31414
|
+
recommendationInputs() {
|
|
31415
|
+
const rows = allRows(
|
|
31416
|
+
this.db.prepare(
|
|
31417
|
+
`SELECT d.rule_id AS rule_id,
|
|
31418
|
+
d.category AS category,
|
|
31419
|
+
d.severity AS severity,
|
|
31420
|
+
COUNT(*) AS count
|
|
31421
|
+
FROM inspection_findings f
|
|
31422
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31423
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
31424
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
31425
|
+
ON latest.finding_key = f.finding_key
|
|
31426
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
31427
|
+
AND e.event_type = 'code_change'
|
|
31428
|
+
AND (
|
|
31429
|
+
f.finding_key IS NULL
|
|
31430
|
+
OR latest.status IS NULL
|
|
31431
|
+
OR latest.status NOT IN ('resolved', 'dismissed')
|
|
31432
|
+
)
|
|
31433
|
+
GROUP BY d.rule_id, d.category, d.severity`
|
|
31434
|
+
)
|
|
31435
|
+
);
|
|
31436
|
+
return Promise.resolve(
|
|
31437
|
+
rows.map((r) => ({
|
|
31438
|
+
ruleId: r.rule_id,
|
|
31439
|
+
category: r.category,
|
|
31440
|
+
severity: r.severity,
|
|
31441
|
+
count: r.count
|
|
31442
|
+
}))
|
|
31443
|
+
);
|
|
31444
|
+
}
|
|
31193
31445
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
31194
31446
|
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
31195
31447
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
31196
31448
|
findingsInRange(fromMs, toMs) {
|
|
31197
31449
|
const rows = allRows(
|
|
31198
31450
|
this.db.prepare(
|
|
31199
|
-
`
|
|
31451
|
+
// `rule_id`/`category` cost nothing extra: inspection_definitions is already
|
|
31452
|
+
// joined for `severity`, so they are two more columns off a row this read
|
|
31453
|
+
// already fetches. They feed the recommended-actions rollup.
|
|
31454
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
|
|
31455
|
+
d.rule_id AS rule_id, d.category AS category
|
|
31200
31456
|
FROM inspection_findings f
|
|
31201
31457
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31202
31458
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -31209,7 +31465,9 @@ var SqliteSecurityRepository = class {
|
|
|
31209
31465
|
return rows.map((r) => ({
|
|
31210
31466
|
occurredAt: r.occurred_at,
|
|
31211
31467
|
severity: r.severity,
|
|
31212
|
-
actionTaken: r.action_taken
|
|
31468
|
+
actionTaken: r.action_taken,
|
|
31469
|
+
ruleId: r.rule_id,
|
|
31470
|
+
category: r.category
|
|
31213
31471
|
}));
|
|
31214
31472
|
}
|
|
31215
31473
|
};
|
|
@@ -32379,8 +32637,45 @@ function openLocalDatabase(dir) {
|
|
|
32379
32637
|
};
|
|
32380
32638
|
}
|
|
32381
32639
|
|
|
32382
|
-
// ../../packages/persistence/src/
|
|
32640
|
+
// ../../packages/persistence/src/egress-wire.ts
|
|
32383
32641
|
import { createHash as createHash3 } from "crypto";
|
|
32642
|
+
function hashProjectKey(projectKey) {
|
|
32643
|
+
return createHash3("sha256").update(projectKey, "utf8").digest("hex");
|
|
32644
|
+
}
|
|
32645
|
+
function toIngestHit(hit) {
|
|
32646
|
+
return {
|
|
32647
|
+
host: hit.host,
|
|
32648
|
+
kind: hit.kind,
|
|
32649
|
+
name: hit.name,
|
|
32650
|
+
category: hit.category,
|
|
32651
|
+
trust: hit.trust,
|
|
32652
|
+
network: hit.network,
|
|
32653
|
+
method: hit.method,
|
|
32654
|
+
transport: hit.transport,
|
|
32655
|
+
url: hit.url,
|
|
32656
|
+
template: hit.template,
|
|
32657
|
+
dataClass: hit.dataClass,
|
|
32658
|
+
site: {
|
|
32659
|
+
file: hit.site.file,
|
|
32660
|
+
line: hit.site.line,
|
|
32661
|
+
dynamic: hit.site.dynamic,
|
|
32662
|
+
vendored: hit.site.vendored
|
|
32663
|
+
}
|
|
32664
|
+
};
|
|
32665
|
+
}
|
|
32666
|
+
function toEgressIngestRequest(input2) {
|
|
32667
|
+
const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
|
|
32668
|
+
const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
|
|
32669
|
+
return {
|
|
32670
|
+
projectKey: hashProjectKey(input2.projectKey),
|
|
32671
|
+
project: input2.project,
|
|
32672
|
+
reconcile,
|
|
32673
|
+
hits: hits.map(toIngestHit)
|
|
32674
|
+
};
|
|
32675
|
+
}
|
|
32676
|
+
|
|
32677
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
32678
|
+
import { createHash as createHash4 } from "crypto";
|
|
32384
32679
|
|
|
32385
32680
|
// ../../packages/persistence/src/fingerprint.ts
|
|
32386
32681
|
import { createHmac, randomBytes } from "crypto";
|
|
@@ -32421,14 +32716,18 @@ function readFingerprintKey(dataDir2) {
|
|
|
32421
32716
|
return parseKeyFile(raw);
|
|
32422
32717
|
}
|
|
32423
32718
|
|
|
32424
|
-
// ../../packages/persistence/src/history-
|
|
32719
|
+
// ../../packages/persistence/src/history-backfill.ts
|
|
32425
32720
|
import { existsSync as existsSync4 } from "fs";
|
|
32426
32721
|
import { join as join9 } from "path";
|
|
32722
|
+
|
|
32723
|
+
// ../../packages/persistence/src/history-preview.ts
|
|
32724
|
+
import { existsSync as existsSync5 } from "fs";
|
|
32725
|
+
import { join as join10 } from "path";
|
|
32427
32726
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
32428
32727
|
|
|
32429
32728
|
// ../../packages/persistence/src/store-symlinks.ts
|
|
32430
|
-
import { existsSync as
|
|
32431
|
-
import { dirname as dirname3, join as
|
|
32729
|
+
import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
|
|
32730
|
+
import { dirname as dirname3, join as join11, resolve } from "path";
|
|
32432
32731
|
|
|
32433
32732
|
// ../../packages/persistence/src/vault/crypto.ts
|
|
32434
32733
|
import {
|
|
@@ -32443,62 +32742,25 @@ import {
|
|
|
32443
32742
|
import { execFileSync } from "child_process";
|
|
32444
32743
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
32445
32744
|
import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
32446
|
-
import { join as
|
|
32745
|
+
import { join as join12 } from "path";
|
|
32447
32746
|
|
|
32448
32747
|
// ../../packages/persistence/src/vault/vault.ts
|
|
32449
32748
|
import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
|
|
32450
32749
|
|
|
32451
32750
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
32452
|
-
import { existsSync as
|
|
32453
|
-
import { join as
|
|
32751
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
32752
|
+
import { join as join13 } from "path";
|
|
32454
32753
|
var MARKER = "warn-era-capped";
|
|
32455
32754
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
32456
32755
|
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
32457
|
-
const marker =
|
|
32458
|
-
if (
|
|
32756
|
+
const marker = join13(dataDir2, MARKER);
|
|
32757
|
+
if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
|
|
32459
32758
|
const capped = db.policies.capCategoryActions();
|
|
32460
32759
|
writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
|
|
32461
32760
|
`, { mode: DATA_FILE_MODE });
|
|
32462
32761
|
return { capped };
|
|
32463
32762
|
}
|
|
32464
32763
|
|
|
32465
|
-
// ../../packages/plugin-runtime/src/attached/egress-wire.ts
|
|
32466
|
-
import { createHash as createHash4 } from "crypto";
|
|
32467
|
-
function hashProjectKey(projectKey) {
|
|
32468
|
-
return createHash4("sha256").update(projectKey, "utf8").digest("hex");
|
|
32469
|
-
}
|
|
32470
|
-
function toIngestHit(hit) {
|
|
32471
|
-
return {
|
|
32472
|
-
host: hit.host,
|
|
32473
|
-
kind: hit.kind,
|
|
32474
|
-
name: hit.name,
|
|
32475
|
-
category: hit.category,
|
|
32476
|
-
trust: hit.trust,
|
|
32477
|
-
network: hit.network,
|
|
32478
|
-
method: hit.method,
|
|
32479
|
-
transport: hit.transport,
|
|
32480
|
-
url: hit.url,
|
|
32481
|
-
template: hit.template,
|
|
32482
|
-
dataClass: hit.dataClass,
|
|
32483
|
-
site: {
|
|
32484
|
-
file: hit.site.file,
|
|
32485
|
-
line: hit.site.line,
|
|
32486
|
-
dynamic: hit.site.dynamic,
|
|
32487
|
-
vendored: hit.site.vendored
|
|
32488
|
-
}
|
|
32489
|
-
};
|
|
32490
|
-
}
|
|
32491
|
-
function toEgressIngestRequest(input2) {
|
|
32492
|
-
const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
|
|
32493
|
-
const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
|
|
32494
|
-
return {
|
|
32495
|
-
projectKey: hashProjectKey(input2.projectKey),
|
|
32496
|
-
project: input2.project,
|
|
32497
|
-
reconcile,
|
|
32498
|
-
hits: hits.map(toIngestHit)
|
|
32499
|
-
};
|
|
32500
|
-
}
|
|
32501
|
-
|
|
32502
32764
|
// ../../packages/remote/src/http.ts
|
|
32503
32765
|
import { request as httpRequest } from "http";
|
|
32504
32766
|
import { request as httpsRequest } from "https";
|
|
@@ -32778,6 +33040,7 @@ function createRemoteClient(options) {
|
|
|
32778
33040
|
url: url2(ROUTES.shares),
|
|
32779
33041
|
body: JSON.stringify(validated.data)
|
|
32780
33042
|
});
|
|
33043
|
+
if (response.status === 404) throw new RemoteRouteAbsent(ROUTES.shares);
|
|
32781
33044
|
okBody(response);
|
|
32782
33045
|
},
|
|
32783
33046
|
async pollCommand() {
|
|
@@ -32800,19 +33063,51 @@ function createRemoteClient(options) {
|
|
|
32800
33063
|
};
|
|
32801
33064
|
}
|
|
32802
33065
|
|
|
32803
|
-
// ../../packages/
|
|
33066
|
+
// ../../packages/remote/src/failure-kind.ts
|
|
32804
33067
|
function statusOf(err) {
|
|
32805
33068
|
if (typeof err !== "object" || err === null || !("status" in err)) return null;
|
|
32806
33069
|
const { status } = err;
|
|
32807
33070
|
if (typeof status !== "number" || !Number.isInteger(status)) return null;
|
|
32808
33071
|
return status >= 100 && status <= 599 ? status : null;
|
|
32809
33072
|
}
|
|
32810
|
-
function
|
|
32811
|
-
|
|
33073
|
+
function nameOf(err) {
|
|
33074
|
+
if (typeof err !== "object" || err === null || !("name" in err)) return null;
|
|
33075
|
+
return typeof err.name === "string" ? err.name : null;
|
|
33076
|
+
}
|
|
33077
|
+
function classifyRemoteFailure(err) {
|
|
33078
|
+
switch (nameOf(err)) {
|
|
33079
|
+
case "RemoteRouteAbsent":
|
|
33080
|
+
return "route-absent";
|
|
33081
|
+
case "RemoteRequestInvalid":
|
|
33082
|
+
return "invalid-request";
|
|
33083
|
+
case "RemoteResponseInvalid":
|
|
33084
|
+
return "rejected";
|
|
33085
|
+
default:
|
|
33086
|
+
break;
|
|
33087
|
+
}
|
|
33088
|
+
const status = statusOf(err);
|
|
33089
|
+
if (status === null) return "unreachable";
|
|
33090
|
+
switch (status) {
|
|
32812
33091
|
case 401:
|
|
32813
33092
|
return "unauthorized";
|
|
32814
33093
|
case 403:
|
|
32815
33094
|
return "forbidden";
|
|
33095
|
+
case 429:
|
|
33096
|
+
return "unreachable";
|
|
33097
|
+
case 404:
|
|
33098
|
+
return "unreachable";
|
|
33099
|
+
default:
|
|
33100
|
+
return status >= 400 && status <= 499 ? "rejected" : "unreachable";
|
|
33101
|
+
}
|
|
33102
|
+
}
|
|
33103
|
+
|
|
33104
|
+
// ../../packages/plugin-runtime/src/attached/failure.ts
|
|
33105
|
+
function classifyFailure(err) {
|
|
33106
|
+
switch (classifyRemoteFailure(err)) {
|
|
33107
|
+
case "unauthorized":
|
|
33108
|
+
return "unauthorized";
|
|
33109
|
+
case "forbidden":
|
|
33110
|
+
return "forbidden";
|
|
32816
33111
|
default:
|
|
32817
33112
|
return "unreachable";
|
|
32818
33113
|
}
|
|
@@ -32835,10 +33130,10 @@ function withTimeout(promise2, ms) {
|
|
|
32835
33130
|
|
|
32836
33131
|
// ../../packages/plugin-runtime/src/attached/forward-drops.ts
|
|
32837
33132
|
import { readFileSync as readFileSync8 } from "fs";
|
|
32838
|
-
import { join as
|
|
33133
|
+
import { join as join14 } from "path";
|
|
32839
33134
|
var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
|
|
32840
33135
|
function forwardDropsPath(dataDir2) {
|
|
32841
|
-
return
|
|
33136
|
+
return join14(dataDir2, FORWARD_DROPS_FILENAME);
|
|
32842
33137
|
}
|
|
32843
33138
|
function recordForwardDrops(dataDir2, count, nowMs) {
|
|
32844
33139
|
if (count <= 0) return;
|
|
@@ -32874,13 +33169,13 @@ function readForwardDrops(dataDir2) {
|
|
|
32874
33169
|
|
|
32875
33170
|
// ../../packages/plugin-runtime/src/attached/forward-policy.ts
|
|
32876
33171
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
32877
|
-
import { readFileSync as
|
|
33172
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
32878
33173
|
import { readFile, rename, writeFile } from "fs/promises";
|
|
32879
|
-
import { join as
|
|
33174
|
+
import { join as join24 } from "path";
|
|
32880
33175
|
|
|
32881
33176
|
// ../../packages/plugin-sdk/src/config.ts
|
|
32882
|
-
import { existsSync as
|
|
32883
|
-
import { join as
|
|
33177
|
+
import { existsSync as existsSync8 } from "fs";
|
|
33178
|
+
import { join as join15 } from "path";
|
|
32884
33179
|
|
|
32885
33180
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
32886
33181
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
@@ -32934,8 +33229,8 @@ function resolveProvider() {
|
|
|
32934
33229
|
function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
|
|
32935
33230
|
try {
|
|
32936
33231
|
ensureLayoutDirSync(base);
|
|
32937
|
-
const settingsFile =
|
|
32938
|
-
if (
|
|
33232
|
+
const settingsFile = join15(settingsDir(base), "settings.json");
|
|
33233
|
+
if (existsSync8(settingsFile)) tightenFile(settingsFile);
|
|
32939
33234
|
} catch {
|
|
32940
33235
|
}
|
|
32941
33236
|
migrateLegacyLayout(base);
|
|
@@ -32960,7 +33255,7 @@ function resolveProviderSafe(resolveProviderFn) {
|
|
|
32960
33255
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
32961
33256
|
import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
|
|
32962
33257
|
import { homedir as homedir2 } from "os";
|
|
32963
|
-
import { basename as basename3, join as
|
|
33258
|
+
import { basename as basename3, join as join17 } from "path";
|
|
32964
33259
|
|
|
32965
33260
|
// ../../packages/detections/src/egress/registry.ts
|
|
32966
33261
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -35743,24 +36038,20 @@ function bundledDetections() {
|
|
|
35743
36038
|
}
|
|
35744
36039
|
|
|
35745
36040
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
35746
|
-
import { existsSync as
|
|
35747
|
-
import { basename as basename2, dirname as dirname4, isAbsolute, join as
|
|
36041
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
36042
|
+
import { basename as basename2, dirname as dirname4, isAbsolute, join as join16, sep as sep2 } from "path";
|
|
35748
36043
|
|
|
35749
36044
|
// ../../packages/plugin-sdk/src/events.ts
|
|
35750
36045
|
import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
|
|
35751
36046
|
|
|
35752
36047
|
// ../../packages/plugin-sdk/src/isolated-scan.ts
|
|
35753
|
-
import { existsSync as
|
|
36048
|
+
import { existsSync as existsSync10 } from "fs";
|
|
35754
36049
|
import { fileURLToPath } from "url";
|
|
35755
36050
|
import { Worker } from "worker_threads";
|
|
35756
36051
|
|
|
35757
|
-
// ../../packages/plugin-sdk/src/
|
|
35758
|
-
|
|
35759
|
-
import {
|
|
35760
|
-
import { join as join17 } from "path";
|
|
35761
|
-
|
|
35762
|
-
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
35763
|
-
import { arch, hostname as hostname4, platform, release } from "os";
|
|
36052
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
36053
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
36054
|
+
import { join as join19 } from "path";
|
|
35764
36055
|
|
|
35765
36056
|
// ../../packages/plugin-sdk/src/model-governance.ts
|
|
35766
36057
|
import {
|
|
@@ -35768,24 +36059,99 @@ import {
|
|
|
35768
36059
|
fstatSync,
|
|
35769
36060
|
mkdirSync as mkdirSync2,
|
|
35770
36061
|
openSync as openSync2,
|
|
35771
|
-
readFileSync as
|
|
36062
|
+
readFileSync as readFileSync11,
|
|
35772
36063
|
readSync,
|
|
35773
36064
|
writeFileSync as writeFileSync5
|
|
35774
36065
|
} from "fs";
|
|
35775
36066
|
import { join as join18 } from "path";
|
|
35776
36067
|
var TAIL_BYTES = 256 * 1024;
|
|
35777
36068
|
|
|
36069
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
36070
|
+
var HOST_FEATURE = {
|
|
36071
|
+
ModelSwitch: "model-switch",
|
|
36072
|
+
VaultPointerDisplay: "vault-pointer-display"
|
|
36073
|
+
};
|
|
36074
|
+
var HOST_FLOORS = {
|
|
36075
|
+
[HOST_FEATURE.ModelSwitch]: {
|
|
36076
|
+
label: "model-switch protection",
|
|
36077
|
+
hookEvents: ["PreModelSwitch", "PostModelSwitch"],
|
|
36078
|
+
since: "2.1.251"
|
|
36079
|
+
},
|
|
36080
|
+
[HOST_FEATURE.VaultPointerDisplay]: {
|
|
36081
|
+
label: "vault pointer display",
|
|
36082
|
+
hookEvents: ["MessageDisplay"],
|
|
36083
|
+
since: "2.1.152"
|
|
36084
|
+
}
|
|
36085
|
+
};
|
|
36086
|
+
function hostFloorGaps(hostVersion) {
|
|
36087
|
+
if (hostVersion === void 0) return [];
|
|
36088
|
+
const gaps = [];
|
|
36089
|
+
for (const [feature, row] of Object.entries(HOST_FLOORS)) {
|
|
36090
|
+
if (compareBinaryVersions(hostVersion, row.since) < 0) {
|
|
36091
|
+
gaps.push({ feature, label: row.label, since: row.since });
|
|
36092
|
+
}
|
|
36093
|
+
}
|
|
36094
|
+
return gaps;
|
|
36095
|
+
}
|
|
36096
|
+
function requiredHostVersion(gaps) {
|
|
36097
|
+
let highest;
|
|
36098
|
+
for (const gap of gaps) {
|
|
36099
|
+
if (highest === void 0 || compareBinaryVersions(gap.since, highest) > 0) highest = gap.since;
|
|
36100
|
+
}
|
|
36101
|
+
return highest;
|
|
36102
|
+
}
|
|
36103
|
+
var MAX_TESTED_HOST = "2.1.260";
|
|
36104
|
+
function hostCeilingNotice(hostVersion) {
|
|
36105
|
+
if (hostVersion === void 0) return null;
|
|
36106
|
+
if (compareBinaryVersions(hostVersion, MAX_TESTED_HOST) <= 0) return null;
|
|
36107
|
+
return `This Claude Code (${hostVersion}) is newer than AKA has been tested against (${MAX_TESTED_HOST}). If something here looks wrong, that is the likely cause \u2014 we'll look into it.`;
|
|
36108
|
+
}
|
|
36109
|
+
function hostCompatibilityLines(cache) {
|
|
36110
|
+
if (cache === null) return [];
|
|
36111
|
+
const lines = [`Claude Code: ${cache.version} (last seen)`];
|
|
36112
|
+
const gaps = hostFloorGaps(cache.version);
|
|
36113
|
+
const required2 = requiredHostVersion(gaps);
|
|
36114
|
+
if (required2 !== void 0) {
|
|
36115
|
+
lines.push(` inactive: ${gaps.map((g) => g.label).join(", ")}`);
|
|
36116
|
+
lines.push(` update Claude Code to ${required2} or newer to turn them on`);
|
|
36117
|
+
}
|
|
36118
|
+
const ceiling = hostCeilingNotice(cache.version);
|
|
36119
|
+
if (ceiling !== null) lines.push(` ${ceiling}`);
|
|
36120
|
+
return lines;
|
|
36121
|
+
}
|
|
36122
|
+
var HOST_VERSION_MARKER = "host-version.json";
|
|
36123
|
+
function readHostVersionCache(dataDir2) {
|
|
36124
|
+
try {
|
|
36125
|
+
const parsed2 = JSON.parse(readFileSync12(join19(dataDir2, HOST_VERSION_MARKER), "utf8"));
|
|
36126
|
+
if (typeof parsed2 !== "object" || parsed2 === null) return null;
|
|
36127
|
+
const { version: version2, observedAt } = parsed2;
|
|
36128
|
+
if (typeof version2 !== "string" || !isParseableBinaryVersion(version2)) return null;
|
|
36129
|
+
if (typeof observedAt !== "number" || !Number.isFinite(observedAt)) return null;
|
|
36130
|
+
return { version: version2, observedAt };
|
|
36131
|
+
} catch {
|
|
36132
|
+
return null;
|
|
36133
|
+
}
|
|
36134
|
+
}
|
|
36135
|
+
|
|
36136
|
+
// ../../packages/plugin-sdk/src/ignore-layers.ts
|
|
36137
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
36138
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
36139
|
+
import { join as join20 } from "path";
|
|
36140
|
+
|
|
36141
|
+
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
36142
|
+
import { arch, hostname as hostname4, platform, release } from "os";
|
|
36143
|
+
|
|
35778
36144
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
35779
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
35780
|
-
import { join as
|
|
36145
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
|
|
36146
|
+
import { join as join21 } from "path";
|
|
35781
36147
|
|
|
35782
36148
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
35783
36149
|
import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
|
|
35784
36150
|
import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
|
|
35785
36151
|
|
|
35786
36152
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
35787
|
-
import { existsSync as
|
|
35788
|
-
import { basename as basename5, join as
|
|
36153
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
|
|
36154
|
+
import { basename as basename5, join as join22 } from "path";
|
|
35789
36155
|
|
|
35790
36156
|
// ../../packages/plugin-sdk/src/provider-env-antigravity.ts
|
|
35791
36157
|
var optionalBaseUrl2 = external_exports.preprocess((v) => {
|
|
@@ -35821,7 +36187,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
35821
36187
|
|
|
35822
36188
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
35823
36189
|
import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
35824
|
-
import { join as
|
|
36190
|
+
import { join as join23 } from "path";
|
|
35825
36191
|
|
|
35826
36192
|
// ../../packages/plugin-runtime/src/attached/forward-policy.ts
|
|
35827
36193
|
function isInvalidRequest(err) {
|
|
@@ -35861,7 +36227,7 @@ function parseBreakerState(raw, nowMs) {
|
|
|
35861
36227
|
}
|
|
35862
36228
|
function createForwardPolicy(deps) {
|
|
35863
36229
|
const now = deps.now ?? (() => Date.now());
|
|
35864
|
-
const file2 =
|
|
36230
|
+
const file2 = join24(deps.dir, STATE_FILENAME);
|
|
35865
36231
|
let state = null;
|
|
35866
36232
|
let loading = null;
|
|
35867
36233
|
async function readState() {
|
|
@@ -36583,8 +36949,8 @@ function toolAuditEvent(input2) {
|
|
|
36583
36949
|
}
|
|
36584
36950
|
|
|
36585
36951
|
// ../../packages/plugin-runtime/src/attached/history-state.ts
|
|
36586
|
-
import { readFileSync as
|
|
36587
|
-
import { join as
|
|
36952
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
36953
|
+
import { join as join25 } from "path";
|
|
36588
36954
|
|
|
36589
36955
|
// ../../packages/plugin-runtime/src/attached/history-sync.ts
|
|
36590
36956
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -36601,7 +36967,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
36601
36967
|
var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
|
|
36602
36968
|
|
|
36603
36969
|
// ../../packages/plugin-runtime/src/attached/plugin-block.ts
|
|
36604
|
-
import { readFileSync as
|
|
36970
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
36605
36971
|
function createPluginBlock(build, policyStore) {
|
|
36606
36972
|
return async () => {
|
|
36607
36973
|
const cached2 = await policyStore.read();
|
|
@@ -36620,7 +36986,7 @@ function createPluginBlock(build, policyStore) {
|
|
|
36620
36986
|
// ../../packages/plugin-runtime/src/attached/policy-store.ts
|
|
36621
36987
|
import { randomUUID as randomUUID16 } from "crypto";
|
|
36622
36988
|
import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
36623
|
-
import { join as
|
|
36989
|
+
import { join as join26 } from "path";
|
|
36624
36990
|
|
|
36625
36991
|
// ../../packages/plugin-runtime/src/attached/atomic-publish.ts
|
|
36626
36992
|
import { rename as rename2 } from "fs/promises";
|
|
@@ -36644,7 +37010,7 @@ async function publishByRename(tmp, file2, move = rename2) {
|
|
|
36644
37010
|
|
|
36645
37011
|
// ../../packages/plugin-runtime/src/attached/policy-store.ts
|
|
36646
37012
|
function createPolicyStore(dir = dataDir()) {
|
|
36647
|
-
const file2 =
|
|
37013
|
+
const file2 = join26(dir, "policy-cache.json");
|
|
36648
37014
|
async function read() {
|
|
36649
37015
|
try {
|
|
36650
37016
|
const raw = await readFile2(file2, "utf8");
|
|
@@ -36875,11 +37241,11 @@ function readStorePosture(dbPath2) {
|
|
|
36875
37241
|
// ../../packages/plugin-runtime/src/attached/posture-store.ts
|
|
36876
37242
|
import { randomUUID as randomUUID17 } from "crypto";
|
|
36877
37243
|
import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
36878
|
-
import { join as
|
|
37244
|
+
import { join as join27 } from "path";
|
|
36879
37245
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
36880
37246
|
function createPostureStore(dir = settingsDir(), legacyDir) {
|
|
36881
|
-
const file2 =
|
|
36882
|
-
const legacyFile = legacyDir === void 0 ? null :
|
|
37247
|
+
const file2 = join27(dir, "posture-state.json");
|
|
37248
|
+
const legacyFile = legacyDir === void 0 ? null : join27(legacyDir, "posture-state.json");
|
|
36883
37249
|
async function persist(state) {
|
|
36884
37250
|
await ensureDataDir(dir);
|
|
36885
37251
|
const tmp = `${file2}.${randomUUID17()}.tmp`;
|
|
@@ -36947,8 +37313,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
|
|
|
36947
37313
|
}
|
|
36948
37314
|
|
|
36949
37315
|
// ../../packages/plugin-runtime/src/attached/sync-state.ts
|
|
36950
|
-
import { readFileSync as
|
|
36951
|
-
import { join as
|
|
37316
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
37317
|
+
import { join as join28 } from "path";
|
|
36952
37318
|
|
|
36953
37319
|
// ../../packages/plugin-runtime/src/attached/status.ts
|
|
36954
37320
|
var REFUSAL_LINES = {
|
|
@@ -37536,7 +37902,7 @@ function fenced(body) {
|
|
|
37536
37902
|
|
|
37537
37903
|
// ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
|
|
37538
37904
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
37539
|
-
import { join as
|
|
37905
|
+
import { join as join29 } from "path";
|
|
37540
37906
|
|
|
37541
37907
|
// ../../packages/setup-wizard/src/triage/merge.ts
|
|
37542
37908
|
var RANK = Object.fromEntries(
|
|
@@ -37544,9 +37910,9 @@ var RANK = Object.fromEntries(
|
|
|
37544
37910
|
);
|
|
37545
37911
|
|
|
37546
37912
|
// ../../packages/setup-wizard/src/triage/plan-file.ts
|
|
37547
|
-
import { mkdtempSync, readFileSync as
|
|
37913
|
+
import { mkdtempSync, readFileSync as readFileSync19, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
37548
37914
|
import { tmpdir } from "os";
|
|
37549
|
-
import { basename as basename6, dirname as dirname6, join as
|
|
37915
|
+
import { basename as basename6, dirname as dirname6, join as join30 } from "path";
|
|
37550
37916
|
var SuppressionEntrySchema = external_exports.object({
|
|
37551
37917
|
ruleId: external_exports.string(),
|
|
37552
37918
|
category: DetectionCategory,
|
|
@@ -37593,7 +37959,6 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
37593
37959
|
var COMMANDS_DIR = fileURLToPath4(new URL("../commands", import.meta.url));
|
|
37594
37960
|
|
|
37595
37961
|
// src/render.ts
|
|
37596
|
-
var SEVERITY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
37597
37962
|
var SEVERITY_GLYPH = {
|
|
37598
37963
|
critical: SHADE.full,
|
|
37599
37964
|
high: SHADE.dark,
|
|
@@ -37603,15 +37968,6 @@ var SEVERITY_GLYPH = {
|
|
|
37603
37968
|
function severityGlyph(severity) {
|
|
37604
37969
|
return SEVERITY_GLYPH[severity] ?? SHADE.light;
|
|
37605
37970
|
}
|
|
37606
|
-
var ADVICE = {
|
|
37607
|
-
secret: "Rotate the exposed credentials and move them out of prompts (secrets manager / env vars).",
|
|
37608
|
-
pii: "Remove or mask personal data before it reaches the model.",
|
|
37609
|
-
financial: "Strip card and account numbers; share only non-sensitive references.",
|
|
37610
|
-
phi: "Remove protected health information \u2014 it should never reach an external model.",
|
|
37611
|
-
code_context: "Confirm this proprietary code context is safe to share.",
|
|
37612
|
-
code_flaw: "Review the flagged pattern and apply the secure alternative (parameterized queries, safe deserializers, etc.).",
|
|
37613
|
-
custom: "Review against your organization\u2019s custom policy."
|
|
37614
|
-
};
|
|
37615
37971
|
function shortTime(iso) {
|
|
37616
37972
|
if (!iso) return "\u2014";
|
|
37617
37973
|
return iso.length >= 16 ? `${iso.slice(5, 10)} ${iso.slice(11, 16)}` : iso;
|
|
@@ -37620,11 +37976,6 @@ function empty(message) {
|
|
|
37620
37976
|
return message;
|
|
37621
37977
|
}
|
|
37622
37978
|
var CATEGORY_ORDER2 = DetectionCategory.options;
|
|
37623
|
-
function healthScore(summary) {
|
|
37624
|
-
const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
|
|
37625
|
-
const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
|
|
37626
|
-
return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
|
|
37627
|
-
}
|
|
37628
37979
|
function renderFindings(findings, status, severity) {
|
|
37629
37980
|
if (findings.length === 0) {
|
|
37630
37981
|
return empty(
|
|
@@ -37680,13 +38031,6 @@ function renderStatusBar(s, opts = {}) {
|
|
|
37680
38031
|
const open3 = `${flag} ${String(s.openFindings)} open findings`;
|
|
37681
38032
|
return `${paint.brand("\u25B8\u25B8 AKA")}${sep4}${score}${sep4}${tally}${sep4}${open3}`;
|
|
37682
38033
|
}
|
|
37683
|
-
function findingStatus(summary) {
|
|
37684
|
-
return {
|
|
37685
|
-
score: healthScore(summary),
|
|
37686
|
-
unreviewed: { ...summary.bySeverity },
|
|
37687
|
-
openFindings: summary.findings
|
|
37688
|
-
};
|
|
37689
|
-
}
|
|
37690
38034
|
function renderHealth(r) {
|
|
37691
38035
|
const lines = [`\u25CF ${r.title}`, ""];
|
|
37692
38036
|
for (const g of r.gauges) lines.push(indent(renderGauge(g)));
|
|
@@ -37694,6 +38038,10 @@ function renderHealth(r) {
|
|
|
37694
38038
|
const pct = Math.round(r.scanCoverage * 100);
|
|
37695
38039
|
const stats = `Open findings ${String(r.openFindings)} Scan coverage ${String(pct)}%`;
|
|
37696
38040
|
lines.push(indent(stats), "");
|
|
38041
|
+
if (r.host.length > 0) {
|
|
38042
|
+
for (const line of r.host) lines.push(indent(line));
|
|
38043
|
+
lines.push("");
|
|
38044
|
+
}
|
|
37697
38045
|
lines.push(indent("Detections & actions \u2014 last 7 days"));
|
|
37698
38046
|
const maxDay = Math.max(1, ...r.week.map((d) => d.total));
|
|
37699
38047
|
for (const d of r.week) {
|
|
@@ -37732,7 +38080,7 @@ function weekday(isoDay2) {
|
|
|
37732
38080
|
const date5 = /* @__PURE__ */ new Date(`${isoDay2}T00:00:00Z`);
|
|
37733
38081
|
return Number.isNaN(date5.getTime()) ? isoDay2 : WEEKDAYS[date5.getUTCDay()] ?? isoDay2;
|
|
37734
38082
|
}
|
|
37735
|
-
function buildHealthReport(summary, findings, activity) {
|
|
38083
|
+
function buildHealthReport(summary, findings, activity, hostLines = []) {
|
|
37736
38084
|
const status = findingStatus(summary);
|
|
37737
38085
|
const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
|
|
37738
38086
|
const handledPct = summary.findings === 0 ? 100 : Math.round(handled / summary.findings * 100);
|
|
@@ -37769,48 +38117,10 @@ function buildHealthReport(summary, findings, activity) {
|
|
|
37769
38117
|
// so "review N prioritized actions" always matches that screen.
|
|
37770
38118
|
recommendCount: buildRecommendations(findings).length,
|
|
37771
38119
|
unreviewed: status.unreviewed,
|
|
37772
|
-
score: status.score
|
|
38120
|
+
score: status.score,
|
|
38121
|
+
host: [...hostLines]
|
|
37773
38122
|
};
|
|
37774
38123
|
}
|
|
37775
|
-
var REC_TEMPLATE = {
|
|
37776
|
-
secret: { title: "Exposed secret detected", action: "Rotate" },
|
|
37777
|
-
pii: { title: "Personal data in a prompt", action: "Remove" },
|
|
37778
|
-
financial: { title: "Financial data detected", action: "Strip" },
|
|
37779
|
-
phi: { title: "Health information detected", action: "Remove" },
|
|
37780
|
-
code_context: { title: "Proprietary code shared", action: "Review" },
|
|
37781
|
-
custom: { title: "Custom policy match", action: "Review" }
|
|
37782
|
-
};
|
|
37783
|
-
var MAX_RECOMMENDATIONS = 10;
|
|
37784
|
-
function buildRecommendations(findings) {
|
|
37785
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
37786
|
-
for (const f of findings) {
|
|
37787
|
-
const b = buckets.get(f.category) ?? {
|
|
37788
|
-
category: f.category,
|
|
37789
|
-
count: 0,
|
|
37790
|
-
severity: f.severity,
|
|
37791
|
-
weight: 0,
|
|
37792
|
-
ruleId: f.ruleId
|
|
37793
|
-
};
|
|
37794
|
-
b.count++;
|
|
37795
|
-
const w = SEVERITY_WEIGHT[f.severity] ?? 0;
|
|
37796
|
-
if (w > b.weight) {
|
|
37797
|
-
b.weight = w;
|
|
37798
|
-
b.severity = f.severity;
|
|
37799
|
-
b.ruleId = f.ruleId;
|
|
37800
|
-
}
|
|
37801
|
-
buckets.set(f.category, b);
|
|
37802
|
-
}
|
|
37803
|
-
return [...buckets.values()].sort((a, b) => b.weight - a.weight || b.count - a.count).slice(0, MAX_RECOMMENDATIONS).map((b) => {
|
|
37804
|
-
const t = REC_TEMPLATE[b.category] ?? { title: `${b.category} finding`, action: "Review" };
|
|
37805
|
-
return {
|
|
37806
|
-
severity: b.severity,
|
|
37807
|
-
title: t.title,
|
|
37808
|
-
description: ADVICE[b.category] ?? "Review this finding against your policy.",
|
|
37809
|
-
context: `${b.ruleId} \xB7 ${String(b.count)} finding${b.count === 1 ? "" : "s"}`,
|
|
37810
|
-
action: t.action
|
|
37811
|
-
};
|
|
37812
|
-
});
|
|
37813
|
-
}
|
|
37814
38124
|
var REC_DESC_WIDTH = 72;
|
|
37815
38125
|
var REC_BODY_INDENT = " ";
|
|
37816
38126
|
function renderRecommend(recs, status) {
|
|
@@ -37991,7 +38301,7 @@ async function runQuery(sub2, gateway, opts = {}) {
|
|
|
37991
38301
|
gateway.recentFindings({ limit: 500 }),
|
|
37992
38302
|
gateway.activityByDay(7)
|
|
37993
38303
|
]);
|
|
37994
|
-
return renderHealth(buildHealthReport(summary, findings, activity));
|
|
38304
|
+
return renderHealth(buildHealthReport(summary, findings, activity, opts.hostLines));
|
|
37995
38305
|
}
|
|
37996
38306
|
case "recommend": {
|
|
37997
38307
|
const [findings, summary] = await Promise.all([
|
|
@@ -38047,7 +38357,16 @@ try {
|
|
|
38047
38357
|
try {
|
|
38048
38358
|
const severity = parseSeverity(args);
|
|
38049
38359
|
process.stdout.write(
|
|
38050
|
-
`${fenced(
|
|
38360
|
+
`${fenced(
|
|
38361
|
+
await runQuery(sub, gateway, {
|
|
38362
|
+
...severity !== void 0 ? { severity } : {},
|
|
38363
|
+
// Resolved here rather than inside runQuery, which holds a gateway
|
|
38364
|
+
// and not the data dir. Read from the cache a hook wrote: probing
|
|
38365
|
+
// `claude --version` would answer for the install on PATH, which
|
|
38366
|
+
// need not be the one running any session.
|
|
38367
|
+
hostLines: hostCompatibilityLines(readHostVersionCache(config2.dataDir))
|
|
38368
|
+
})
|
|
38369
|
+
)}
|
|
38051
38370
|
`
|
|
38052
38371
|
);
|
|
38053
38372
|
} finally {
|