@akasecurity/cli 0.8.2 → 0.9.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli.js +412 -97
- package/package.json +10 -4
- package/web-ui/rules/core-code-context/db-table-name.json +0 -1
- package/web-ui/web-ui/.next/BUILD_ID +1 -1
- package/web-ui/web-ui/.next/app-build-manifest.json +54 -53
- package/web-ui/web-ui/.next/app-path-routes-manifest.json +5 -5
- package/web-ui/web-ui/.next/build-manifest.json +2 -2
- package/web-ui/web-ui/.next/prerender-manifest.json +15 -15
- package/web-ui/web-ui/.next/required-server-files.json +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found.rsc +2 -2
- package/web-ui/web-ui/.next/server/app/index.html +1 -1
- package/web-ui/web-ui/.next/server/app/index.rsc +2 -2
- package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app-paths-manifest.json +5 -5
- package/web-ui/web-ui/.next/server/chunks/366.js +73 -57
- package/web-ui/web-ui/.next/server/chunks/45.js +1 -1
- package/web-ui/web-ui/.next/server/chunks/461.js +5 -5
- package/web-ui/web-ui/.next/server/chunks/484.js +72 -56
- package/web-ui/web-ui/.next/server/chunks/773.js +4 -4
- package/web-ui/web-ui/.next/server/chunks/900.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/918.js +2 -0
- package/web-ui/web-ui/.next/server/functions-config-manifest.json +5 -5
- package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
- package/web-ui/web-ui/.next/server/pages/404.html +1 -1
- package/web-ui/web-ui/.next/server/pages/500.html +1 -1
- package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
- package/web-ui/web-ui/.next/static/chunks/{24-b20c857178647805.js → 24-55b96836aea5ce23.js} +3 -3
- package/web-ui/web-ui/.next/static/chunks/561-c3388f066f813e17.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-0c208c8b275a69ac.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/{page-d469bd704615ea76.js → page-041187c76bd42461.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-481fedcbfb474cef.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-a20e7d7a2f0ad507.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/{page-d6cfd847bb528eec.js → page-2cfe42c3dd4833c3.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-849d66cc2cd77cb6.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-d33f3999e89c7e77.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-76edd10592ba3cb1.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-d15081016f40ac92.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/{page-b00573441ac43bdf.js → page-50b604f229747240.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/{page-3b5fe92cd995b763.js → page-92d1086c8e8ed3bf.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-c3c1671d32547619.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/{layout-8b19b03146218dc7.js → layout-40fea3e270a8e120.js} +1 -1
- package/web-ui/web-ui/.next/static/css/43f95c94ecc1bd17.css +2 -0
- package/web-ui/web-ui/server.js +1 -1
- package/web-ui/web-ui/.next/server/chunks/640.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/913.js +0 -2
- package/web-ui/web-ui/.next/static/chunks/561-46ea4b9ad0419130.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-49c63d0cec0da80d.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07ca4b0ce174ef7a.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-16d5fd7d373c8f0b.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-291617f3a4d46a01.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-c59135e0cfa8d5c7.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-0eb54f5acb30a73e.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-a86819fe3aeb2491.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-50f828314c9a5d91.js +0 -1
- package/web-ui/web-ui/.next/static/css/dfdcbdce6b7c9eb0.css +0 -2
- /package/web-ui/web-ui/.next/static/{zNaHfOWePiRki0ugHKYzl → unUYwnPVSRwxtrUFgSAxr}/_buildManifest.js +0 -0
- /package/web-ui/web-ui/.next/static/{zNaHfOWePiRki0ugHKYzl → unUYwnPVSRwxtrUFgSAxr}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -755,21 +755,28 @@ import { createHash, randomUUID } from "crypto";
|
|
|
755
755
|
import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
756
756
|
import { join as join2, relative, sep } from "path";
|
|
757
757
|
|
|
758
|
+
// ../packages/detections/src/escape-regexp.ts
|
|
759
|
+
function escapeRegExp(value) {
|
|
760
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ../packages/detections/src/matchers/limits.ts
|
|
764
|
+
var MAX_MATCHES_PER_RULE = 1e4;
|
|
765
|
+
|
|
758
766
|
// ../packages/detections/src/matchers/keyword.ts
|
|
759
767
|
var KeywordMatcher = class {
|
|
760
768
|
match(text, rule) {
|
|
761
769
|
if (rule.matcher.type !== "keyword") return [];
|
|
762
770
|
const { keywords, caseSensitive } = rule.matcher;
|
|
763
|
-
const haystack = caseSensitive ? text : text.toLowerCase();
|
|
764
771
|
const spans = [];
|
|
765
772
|
for (const kw of keywords) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
spans.push({ start:
|
|
772
|
-
|
|
773
|
+
if (kw.length === 0) continue;
|
|
774
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
775
|
+
const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
|
|
776
|
+
let m;
|
|
777
|
+
while ((m = re.exec(text)) !== null) {
|
|
778
|
+
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
779
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
773
780
|
}
|
|
774
781
|
}
|
|
775
782
|
return spans;
|
|
@@ -777,7 +784,6 @@ var KeywordMatcher = class {
|
|
|
777
784
|
};
|
|
778
785
|
|
|
779
786
|
// ../packages/detections/src/matchers/regex.ts
|
|
780
|
-
var MAX_MATCHES_PER_RULE = 1e4;
|
|
781
787
|
var RegexMatcher = class {
|
|
782
788
|
match(text, rule) {
|
|
783
789
|
if (rule.matcher.type !== "regex") return [];
|
|
@@ -863,9 +869,6 @@ function registerPack(pack) {
|
|
|
863
869
|
function getLoadedRules() {
|
|
864
870
|
return [...packs.values()].flatMap((p) => p.rules);
|
|
865
871
|
}
|
|
866
|
-
function escapeRegExp(value) {
|
|
867
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
868
|
-
}
|
|
869
872
|
function isCorroborated(candidate, candidates, text) {
|
|
870
873
|
const req = candidate.rule.requiresNearby;
|
|
871
874
|
if (!req) return true;
|
|
@@ -1098,6 +1101,10 @@ var SQLITE_MIGRATIONS = [
|
|
|
1098
1101
|
{
|
|
1099
1102
|
tag: "0009_findings_path_expression_index",
|
|
1100
1103
|
sql: "-- Custom migration: partial expression index for the resolver's per-path reads.\n--\n-- openAtRestKeysForPath / resolvedAtRestKeysForPath (and the scanner's tier-3\n-- open-key probe) filter at-rest events by\n-- e.kind = 'code_change' AND json_extract(e.metadata, '$.filePath') = :path\n-- once per changed/deleted file on every scan \u2014 previously a full events scan\n-- per file. json_extract is deterministic, so SQLite allows it in an index;\n-- the WHERE kind = 'code_change' keeps the index to exactly the rows those\n-- queries can match (in-flight events are never path-addressed).\nCREATE INDEX `idx_events_code_change_path` ON `events` (json_extract(`metadata`, '$.filePath')) WHERE `kind` = 'code_change';\n"
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
tag: "0010_events_session_expression_index",
|
|
1107
|
+
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
1101
1108
|
}
|
|
1102
1109
|
];
|
|
1103
1110
|
|
|
@@ -1160,6 +1167,10 @@ function scopeFromAnswer(answer, now = Date.now()) {
|
|
|
1160
1167
|
};
|
|
1161
1168
|
}
|
|
1162
1169
|
|
|
1170
|
+
// ../packages/schema/src/identity.ts
|
|
1171
|
+
var PRODUCT_NAME = "AKA Security";
|
|
1172
|
+
var PRODUCT_TAGLINE = "We secure agent harnesses at the source.";
|
|
1173
|
+
|
|
1163
1174
|
// ../packages/schema/src/time.ts
|
|
1164
1175
|
function isoToEpochMillis(iso) {
|
|
1165
1176
|
return new Date(iso).getTime();
|
|
@@ -1254,12 +1265,15 @@ var defaultCostModel = {
|
|
|
1254
1265
|
};
|
|
1255
1266
|
|
|
1256
1267
|
// ../packages/schema/src/token/format.ts
|
|
1268
|
+
var COMPACT = new Intl.NumberFormat("en-US", {
|
|
1269
|
+
notation: "compact",
|
|
1270
|
+
maximumFractionDigits: 1
|
|
1271
|
+
});
|
|
1272
|
+
function compactNumber(n) {
|
|
1273
|
+
return COMPACT.format(n);
|
|
1274
|
+
}
|
|
1257
1275
|
function formatTokenCount(n) {
|
|
1258
|
-
|
|
1259
|
-
if (abs < 1e3) return String(n);
|
|
1260
|
-
if (abs < 1e6) return `${(n / 1e3).toFixed(1)}k`;
|
|
1261
|
-
if (abs < 1e9) return `${(n / 1e6).toFixed(1)}M`;
|
|
1262
|
-
return `${(n / 1e9).toFixed(1)}B`;
|
|
1276
|
+
return compactNumber(n);
|
|
1263
1277
|
}
|
|
1264
1278
|
function formatUsd(value) {
|
|
1265
1279
|
return value > 0 && value < 0.1 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
|
|
@@ -15993,6 +16007,12 @@ var FindingInstance = external_exports.object({
|
|
|
15993
16007
|
provider: FindingProvider,
|
|
15994
16008
|
repo: external_exports.string(),
|
|
15995
16009
|
file: external_exports.string(),
|
|
16010
|
+
// Host tool that produced the scanned text (event metadata's toolName).
|
|
16011
|
+
// Present whenever the capturing hook recorded one — including
|
|
16012
|
+
// file-attributed captures (views prefer `file`); its display value is
|
|
16013
|
+
// the location fallback ("via Bash") when no filePath exists. Absent for
|
|
16014
|
+
// legacy rows and non-tool captures (prompts, worktree scans).
|
|
16015
|
+
toolName: external_exports.string().optional(),
|
|
15996
16016
|
// Effective action: override.action ?? actionTaken, translated to FindingAction.
|
|
15997
16017
|
action: FindingAction,
|
|
15998
16018
|
detectedAt: external_exports.iso.datetime(),
|
|
@@ -16049,6 +16069,9 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
16049
16069
|
provider: external_exports.array(FindingProvider).optional(),
|
|
16050
16070
|
action: external_exports.array(FindingAction).optional(),
|
|
16051
16071
|
q: external_exports.string().optional(),
|
|
16072
|
+
// Scope to findings whose event carries this session id (the Activity page's
|
|
16073
|
+
// session → findings drilldown). Findings without a session never match.
|
|
16074
|
+
sessionId: external_exports.string().optional(),
|
|
16052
16075
|
groupBy: external_exports.literal("type").optional(),
|
|
16053
16076
|
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
16054
16077
|
cursor: external_exports.string().optional()
|
|
@@ -16060,7 +16083,13 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
16060
16083
|
}),
|
|
16061
16084
|
facets: FindingFacets,
|
|
16062
16085
|
items: external_exports.array(FindingGroup),
|
|
16063
|
-
nextCursor: external_exports.string().nullable()
|
|
16086
|
+
nextCursor: external_exports.string().nullable(),
|
|
16087
|
+
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
16088
|
+
// many times that rule fired in the session's persisted transcript. Findings
|
|
16089
|
+
// here are deduplicated to unique values while the transcript tally counts
|
|
16090
|
+
// every firing, so the two numbers legitimately differ — this map lets a
|
|
16091
|
+
// session-scoped view show both.
|
|
16092
|
+
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
16064
16093
|
}).meta({ id: "ListGroupedFindingsResponse" });
|
|
16065
16094
|
var ApplyFindingActionRequest = external_exports.object({
|
|
16066
16095
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
@@ -16468,6 +16497,10 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
16468
16497
|
from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
16469
16498
|
/** Upper bound on startedAt; omitted defaults to now. */
|
|
16470
16499
|
to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
16500
|
+
/** Exclude zero-activity sessions — roots whose only recorded children are
|
|
16501
|
+
* bookkeeping rows (hooks, config scans), typically background `claude`
|
|
16502
|
+
* launches. Omitted = list everything. `z.stringbool()` per the note above. */
|
|
16503
|
+
excludeEmpty: external_exports.stringbool().optional(),
|
|
16471
16504
|
/** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
|
|
16472
16505
|
limit: external_exports.coerce.number().int().min(1).max(100).default(50),
|
|
16473
16506
|
/** Opaque pagination cursor (most-recent first). */
|
|
@@ -16476,7 +16509,11 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
16476
16509
|
var ListActivitySessionsResponse = external_exports.object({
|
|
16477
16510
|
items: external_exports.array(ActivitySessionSummary),
|
|
16478
16511
|
/** `null` once the last page is reached. */
|
|
16479
|
-
nextCursor: external_exports.string().nullable()
|
|
16512
|
+
nextCursor: external_exports.string().nullable(),
|
|
16513
|
+
/** Zero-activity sessions matching the query's filters/range (whether or
|
|
16514
|
+
* not `excludeEmpty` dropped them from `items`) — the count a UI toggle
|
|
16515
|
+
* shows when collapsing them. */
|
|
16516
|
+
emptyCount: external_exports.number().int().nonnegative()
|
|
16480
16517
|
}).meta({ id: "ListActivitySessionsResponse" });
|
|
16481
16518
|
var ListSessionEventsQuery = external_exports.object({
|
|
16482
16519
|
/** Default 100, range 1–500. */
|
|
@@ -16511,6 +16548,12 @@ var EventMetadata = external_exports.object({
|
|
|
16511
16548
|
sessionId: external_exports.string().optional(),
|
|
16512
16549
|
repo: external_exports.string().optional(),
|
|
16513
16550
|
filePath: external_exports.string().optional(),
|
|
16551
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
|
|
16552
|
+
// set by the tool-scanning hooks. The tool NAME only — never the tool's
|
|
16553
|
+
// arguments or output, which can carry the very value a finding masked
|
|
16554
|
+
// (metadata is stored unredacted). Gives findings on non-file captures a
|
|
16555
|
+
// display location ("via Bash") when no filePath exists.
|
|
16556
|
+
toolName: external_exports.string().optional(),
|
|
16514
16557
|
// Set (true) by the worktree scanner when the file is excluded by the
|
|
16515
16558
|
// repo's .gitignore. Gitignored files ARE still scanned — local scratch and
|
|
16516
16559
|
// generated code can leak real secrets — but the provenance is recorded so
|
|
@@ -16842,7 +16885,10 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16842
16885
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16843
16886
|
var KeywordMatcher2 = external_exports.object({
|
|
16844
16887
|
type: external_exports.literal("keyword"),
|
|
16845
|
-
|
|
16888
|
+
// An empty keyword matches at every position, yielding one zero-length span
|
|
16889
|
+
// per character. Rejected here because a keyword that matches everything is
|
|
16890
|
+
// never intentional.
|
|
16891
|
+
keywords: external_exports.array(external_exports.string().min(1)).min(1),
|
|
16846
16892
|
caseSensitive: external_exports.boolean().default(false)
|
|
16847
16893
|
});
|
|
16848
16894
|
function isValidRegex(pattern, flags) {
|
|
@@ -17091,6 +17137,10 @@ var ListEventsResponse = external_exports.object({
|
|
|
17091
17137
|
items: external_exports.array(Event),
|
|
17092
17138
|
nextCursor: external_exports.string().nullable()
|
|
17093
17139
|
}).meta({ id: "ListEventsResponse" });
|
|
17140
|
+
var IngestResponse = external_exports.object({
|
|
17141
|
+
accepted: external_exports.number().int().nonnegative(),
|
|
17142
|
+
duplicates: external_exports.number().int().nonnegative()
|
|
17143
|
+
}).meta({ id: "IngestResponse" });
|
|
17094
17144
|
var ListFindingsQuery = external_exports.object({
|
|
17095
17145
|
cursor: external_exports.string().optional(),
|
|
17096
17146
|
limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
|
|
@@ -17480,6 +17530,7 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
17480
17530
|
provider: toApiProvider(r.sourceTool),
|
|
17481
17531
|
repo: r.repo,
|
|
17482
17532
|
file: r.file,
|
|
17533
|
+
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
17483
17534
|
action: toApiAction(effectiveDbAction),
|
|
17484
17535
|
detectedAt: r.occurredAt,
|
|
17485
17536
|
confidence: r.confidence,
|
|
@@ -17552,6 +17603,7 @@ function buildHaystack(g, extra) {
|
|
|
17552
17603
|
g.id,
|
|
17553
17604
|
...g.instances.map((i) => i.repo),
|
|
17554
17605
|
...g.instances.map((i) => i.file),
|
|
17606
|
+
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
17555
17607
|
...g.instances.map((i) => i.id),
|
|
17556
17608
|
...extra === void 0 ? [] : [extra]
|
|
17557
17609
|
].join(" ").toLowerCase();
|
|
@@ -17818,6 +17870,74 @@ var ProjectFilesScan = external_exports.object({
|
|
|
17818
17870
|
scannedAt: external_exports.string()
|
|
17819
17871
|
});
|
|
17820
17872
|
|
|
17873
|
+
// ../packages/schema/src/zod/ranges.ts
|
|
17874
|
+
var TIME_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17875
|
+
var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
|
|
17876
|
+
var DEFAULT_TIME_RANGE = "7d";
|
|
17877
|
+
var RANGE_DAYS = {
|
|
17878
|
+
"7d": 7,
|
|
17879
|
+
"30d": 30,
|
|
17880
|
+
"3m": 90,
|
|
17881
|
+
"6m": 180
|
|
17882
|
+
};
|
|
17883
|
+
var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
|
|
17884
|
+
function parseTimeRange(value) {
|
|
17885
|
+
return TIME_RANGE_OR_DEFAULT.parse(value);
|
|
17886
|
+
}
|
|
17887
|
+
|
|
17888
|
+
// ../packages/schema/src/zod/remediation.ts
|
|
17889
|
+
var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
|
|
17890
|
+
var MaskedFindingLocation = external_exports.object({
|
|
17891
|
+
filePath: external_exports.string(),
|
|
17892
|
+
span: Span.optional()
|
|
17893
|
+
}).strict();
|
|
17894
|
+
var MaskedSecretFinding = external_exports.object({
|
|
17895
|
+
provider: external_exports.string(),
|
|
17896
|
+
maskedToken: external_exports.string(),
|
|
17897
|
+
where: MaskedFindingLocation,
|
|
17898
|
+
state: SecretFindingState,
|
|
17899
|
+
observedAt: external_exports.iso.datetime().optional()
|
|
17900
|
+
}).strict();
|
|
17901
|
+
var RotationChecklistEntry = external_exports.object({
|
|
17902
|
+
provider: external_exports.string(),
|
|
17903
|
+
maskedToken: external_exports.string(),
|
|
17904
|
+
consolePath: external_exports.string(),
|
|
17905
|
+
occurrenceSpread: external_exports.number().int().positive()
|
|
17906
|
+
}).strict();
|
|
17907
|
+
var RemediationOption = external_exports.enum([
|
|
17908
|
+
"redact-rotation-checklist",
|
|
17909
|
+
"redact-only",
|
|
17910
|
+
"set-secret-redact",
|
|
17911
|
+
"leave"
|
|
17912
|
+
]);
|
|
17913
|
+
var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
|
|
17914
|
+
var RemediationEntryContext = external_exports.object({
|
|
17915
|
+
entrySource: RemediationEntrySource
|
|
17916
|
+
}).strict();
|
|
17917
|
+
var RemediationOptionChoice = external_exports.object({
|
|
17918
|
+
id: RemediationOption,
|
|
17919
|
+
label: external_exports.string()
|
|
17920
|
+
});
|
|
17921
|
+
var BatchedRemediationDecision = external_exports.object({
|
|
17922
|
+
kind: external_exports.literal("decision"),
|
|
17923
|
+
entrySource: RemediationEntrySource,
|
|
17924
|
+
secretCount: external_exports.number().int().positive(),
|
|
17925
|
+
prompt: external_exports.string(),
|
|
17926
|
+
options: external_exports.tuple([
|
|
17927
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
|
|
17928
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
|
|
17929
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
|
|
17930
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
|
|
17931
|
+
])
|
|
17932
|
+
});
|
|
17933
|
+
var NoRemediationDecision = external_exports.object({
|
|
17934
|
+
kind: external_exports.literal("no-decision")
|
|
17935
|
+
});
|
|
17936
|
+
var BatchedRemediation = external_exports.discriminatedUnion("kind", [
|
|
17937
|
+
BatchedRemediationDecision,
|
|
17938
|
+
NoRemediationDecision
|
|
17939
|
+
]);
|
|
17940
|
+
|
|
17821
17941
|
// ../packages/schema/src/zod/rule-test.ts
|
|
17822
17942
|
var TestRulesRequest = external_exports.object({
|
|
17823
17943
|
rules: external_exports.array(Rule).min(1).max(100),
|
|
@@ -17876,10 +17996,8 @@ var SeveritySummaryResponse = external_exports.object({
|
|
|
17876
17996
|
// All four severity levels are always present (count may be 0).
|
|
17877
17997
|
bySeverity: external_exports.array(SeveritySummaryItem)
|
|
17878
17998
|
}).meta({ id: "SeveritySummaryResponse" });
|
|
17879
|
-
var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17880
|
-
var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
|
|
17881
17999
|
var SecurityRangeQuery = external_exports.object({
|
|
17882
|
-
range: external_exports.enum(
|
|
18000
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
|
|
17883
18001
|
});
|
|
17884
18002
|
var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
|
|
17885
18003
|
var EnforcementAction = external_exports.object({
|
|
@@ -17889,7 +18007,7 @@ var EnforcementAction = external_exports.object({
|
|
|
17889
18007
|
delta: external_exports.number().int()
|
|
17890
18008
|
}).meta({ id: "EnforcementAction" });
|
|
17891
18009
|
var EnforcementActionsResponse = external_exports.object({
|
|
17892
|
-
range:
|
|
18010
|
+
range: TimeRange,
|
|
17893
18011
|
// Sum of actions[].count in the window.
|
|
17894
18012
|
total: external_exports.number().int().nonnegative(),
|
|
17895
18013
|
// One entry per kind, always all three present (count may be 0).
|
|
@@ -17904,7 +18022,7 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
17904
18022
|
medium: external_exports.number().int().nonnegative()
|
|
17905
18023
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
17906
18024
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
17907
|
-
range:
|
|
18025
|
+
range: TimeRange,
|
|
17908
18026
|
granularity: TimeseriesGranularity,
|
|
17909
18027
|
points: external_exports.array(FindingsTimeseriesPoint)
|
|
17910
18028
|
}).meta({ id: "FindingsTimeseriesResponse" });
|
|
@@ -17919,7 +18037,7 @@ var MttrTrendPoint = external_exports.object({
|
|
|
17919
18037
|
})
|
|
17920
18038
|
}).meta({ id: "MttrTrendPoint" });
|
|
17921
18039
|
var MttrTrendResponse = external_exports.object({
|
|
17922
|
-
range:
|
|
18040
|
+
range: TimeRange,
|
|
17923
18041
|
granularity: TimeseriesGranularity,
|
|
17924
18042
|
points: external_exports.array(MttrTrendPoint)
|
|
17925
18043
|
}).meta({ id: "MttrTrendResponse" });
|
|
@@ -17946,11 +18064,11 @@ var TopSource = external_exports.object({
|
|
|
17946
18064
|
findingsCount: external_exports.number().int().nonnegative()
|
|
17947
18065
|
}).meta({ id: "TopSource" });
|
|
17948
18066
|
var TopSourcesResponse = external_exports.object({
|
|
17949
|
-
range:
|
|
18067
|
+
range: TimeRange,
|
|
17950
18068
|
items: external_exports.array(TopSource)
|
|
17951
18069
|
}).meta({ id: "TopSourcesResponse" });
|
|
17952
18070
|
var TopSourcesQuery = external_exports.object({
|
|
17953
|
-
range: external_exports.enum(
|
|
18071
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
|
|
17954
18072
|
limit: external_exports.coerce.number().int().min(1).max(50).default(5),
|
|
17955
18073
|
// Omit for both kinds.
|
|
17956
18074
|
kind: external_exports.enum(SOURCE_KINDS).optional()
|
|
@@ -17963,7 +18081,7 @@ var ScanCoverageProvider = external_exports.object({
|
|
|
17963
18081
|
supported: external_exports.boolean()
|
|
17964
18082
|
}).meta({ id: "ScanCoverageProvider" });
|
|
17965
18083
|
var ScanCoverageResponse = external_exports.object({
|
|
17966
|
-
range:
|
|
18084
|
+
range: TimeRange,
|
|
17967
18085
|
providers: external_exports.array(ScanCoverageProvider)
|
|
17968
18086
|
}).meta({ id: "ScanCoverageResponse" });
|
|
17969
18087
|
var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
|
|
@@ -18012,6 +18130,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
|
|
|
18012
18130
|
var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
|
|
18013
18131
|
var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
|
|
18014
18132
|
|
|
18133
|
+
// ../packages/schema/src/zod/triage.ts
|
|
18134
|
+
var TriageHit = external_exports.object({
|
|
18135
|
+
ruleId: external_exports.string(),
|
|
18136
|
+
category: DetectionCategory,
|
|
18137
|
+
severity: Severity,
|
|
18138
|
+
maskedMatch: external_exports.string(),
|
|
18139
|
+
rawMatch: external_exports.string(),
|
|
18140
|
+
context: external_exports.string(),
|
|
18141
|
+
filePath: external_exports.string().optional(),
|
|
18142
|
+
confidence: external_exports.number().min(0).max(1),
|
|
18143
|
+
id: external_exports.string().optional(),
|
|
18144
|
+
valueFingerprint: external_exports.string().optional(),
|
|
18145
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
18146
|
+
});
|
|
18147
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
18148
|
+
var TriageCategoryRec = external_exports.object({
|
|
18149
|
+
category: DetectionCategory,
|
|
18150
|
+
action: TriagePolicy,
|
|
18151
|
+
reasoning: external_exports.string(),
|
|
18152
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
18153
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
18154
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
18155
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
18156
|
+
fpIds: external_exports.array(external_exports.string())
|
|
18157
|
+
});
|
|
18158
|
+
var TriageRecommendation = external_exports.object({
|
|
18159
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
18160
|
+
notes: external_exports.string()
|
|
18161
|
+
});
|
|
18162
|
+
|
|
18163
|
+
// ../packages/schema/src/zod/setup-frame.ts
|
|
18164
|
+
var CalibrationCounts = external_exports.object({
|
|
18165
|
+
total: external_exports.number().int().nonnegative(),
|
|
18166
|
+
important: external_exports.number().int().nonnegative(),
|
|
18167
|
+
routine: external_exports.number().int().nonnegative()
|
|
18168
|
+
}).refine((c) => c.total === c.important + c.routine, {
|
|
18169
|
+
message: "total must equal important + routine",
|
|
18170
|
+
path: ["total"]
|
|
18171
|
+
});
|
|
18172
|
+
var FalsePositivePatternValue = external_exports.object({
|
|
18173
|
+
ruleId: external_exports.string(),
|
|
18174
|
+
category: DetectionCategory,
|
|
18175
|
+
valueFingerprint: external_exports.string(),
|
|
18176
|
+
keyVersion: external_exports.number().int().nonnegative()
|
|
18177
|
+
});
|
|
18178
|
+
var FalsePositivePatternGroup = external_exports.object({
|
|
18179
|
+
pattern: external_exports.string(),
|
|
18180
|
+
count: external_exports.number().int().nonnegative(),
|
|
18181
|
+
values: external_exports.array(FalsePositivePatternValue).min(1)
|
|
18182
|
+
});
|
|
18183
|
+
var CalibrationFindingKind = external_exports.object({
|
|
18184
|
+
category: DetectionCategory,
|
|
18185
|
+
count: external_exports.number().int().nonnegative(),
|
|
18186
|
+
egress: external_exports.boolean()
|
|
18187
|
+
});
|
|
18188
|
+
var CalibrationFrame = external_exports.object({
|
|
18189
|
+
counts: CalibrationCounts,
|
|
18190
|
+
routineCategories: external_exports.array(DetectionCategory),
|
|
18191
|
+
surfacedCategories: external_exports.array(DetectionCategory),
|
|
18192
|
+
findingKinds: external_exports.array(CalibrationFindingKind),
|
|
18193
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
|
|
18194
|
+
maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
|
|
18195
|
+
falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
|
|
18196
|
+
});
|
|
18197
|
+
var CalibrationPreviewCategory = TriageCategoryRec.pick({
|
|
18198
|
+
category: true,
|
|
18199
|
+
genuineCount: true,
|
|
18200
|
+
fpCount: true
|
|
18201
|
+
}).extend({
|
|
18202
|
+
egress: external_exports.boolean()
|
|
18203
|
+
});
|
|
18204
|
+
var CalibrationPreview = external_exports.object({
|
|
18205
|
+
categories: external_exports.array(CalibrationPreviewCategory),
|
|
18206
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
|
|
18207
|
+
});
|
|
18208
|
+
var CalibrationResult = external_exports.object({
|
|
18209
|
+
frame: CalibrationFrame,
|
|
18210
|
+
copy: external_exports.string()
|
|
18211
|
+
});
|
|
18212
|
+
var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
|
|
18213
|
+
var SetupHandoffOption = external_exports.object({
|
|
18214
|
+
id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
|
|
18215
|
+
label: external_exports.string()
|
|
18216
|
+
});
|
|
18217
|
+
var DashboardHandoffOptions = external_exports.tuple([
|
|
18218
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
18219
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
18220
|
+
]);
|
|
18221
|
+
var ComposedRemediationOptions = external_exports.tuple([
|
|
18222
|
+
SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
|
|
18223
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
18224
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
18225
|
+
]);
|
|
18226
|
+
var SetupHandoffOffer = external_exports.object({
|
|
18227
|
+
worthALook: external_exports.number().int().nonnegative(),
|
|
18228
|
+
liveKeys: external_exports.number().int().nonnegative().optional(),
|
|
18229
|
+
options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
|
|
18230
|
+
}).refine(
|
|
18231
|
+
(o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
|
|
18232
|
+
{
|
|
18233
|
+
message: "the chain-entry option is present exactly when liveKeys > 0",
|
|
18234
|
+
path: ["options"]
|
|
18235
|
+
}
|
|
18236
|
+
).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
|
|
18237
|
+
message: "liveKeys is a subset of worthALook and cannot exceed it",
|
|
18238
|
+
path: ["liveKeys"]
|
|
18239
|
+
});
|
|
18240
|
+
|
|
18015
18241
|
// ../packages/schema/src/zod/shares.ts
|
|
18016
18242
|
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
18017
18243
|
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
@@ -18197,36 +18423,6 @@ function reviewSeverityRank(reasons) {
|
|
|
18197
18423
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
18198
18424
|
}
|
|
18199
18425
|
|
|
18200
|
-
// ../packages/schema/src/zod/triage.ts
|
|
18201
|
-
var TriageHit = external_exports.object({
|
|
18202
|
-
ruleId: external_exports.string(),
|
|
18203
|
-
category: DetectionCategory,
|
|
18204
|
-
severity: Severity,
|
|
18205
|
-
maskedMatch: external_exports.string(),
|
|
18206
|
-
rawMatch: external_exports.string(),
|
|
18207
|
-
context: external_exports.string(),
|
|
18208
|
-
filePath: external_exports.string().optional(),
|
|
18209
|
-
confidence: external_exports.number().min(0).max(1),
|
|
18210
|
-
id: external_exports.string().optional(),
|
|
18211
|
-
valueFingerprint: external_exports.string().optional(),
|
|
18212
|
-
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
18213
|
-
});
|
|
18214
|
-
var TriagePolicy = BuiltinPolicyId;
|
|
18215
|
-
var TriageCategoryRec = external_exports.object({
|
|
18216
|
-
category: DetectionCategory,
|
|
18217
|
-
action: TriagePolicy,
|
|
18218
|
-
reasoning: external_exports.string(),
|
|
18219
|
-
genuineCount: external_exports.number().int().nonnegative(),
|
|
18220
|
-
fpCount: external_exports.number().int().nonnegative(),
|
|
18221
|
-
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
18222
|
-
// this array's length — enforced by the consumer, not this schema.
|
|
18223
|
-
fpIds: external_exports.array(external_exports.string())
|
|
18224
|
-
});
|
|
18225
|
-
var TriageRecommendation = external_exports.object({
|
|
18226
|
-
perCategory: external_exports.array(TriageCategoryRec),
|
|
18227
|
-
notes: external_exports.string()
|
|
18228
|
-
});
|
|
18229
|
-
|
|
18230
18426
|
// ../packages/local-ops/src/fs-scan.ts
|
|
18231
18427
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
18232
18428
|
var AKAIGNORE_FILENAME = ".akaignore";
|
|
@@ -18922,6 +19118,10 @@ var TIMELINE_COLUMNS = `
|
|
|
18922
19118
|
json_extract(attributes, '$.internal') AS internal,
|
|
18923
19119
|
json_extract(attributes, '$.flagged') AS flagged`;
|
|
18924
19120
|
var SESSION_ROOT = `event_type = 'session'`;
|
|
19121
|
+
var HAS_ACTIVITY = `EXISTS (
|
|
19122
|
+
SELECT 1 FROM audit_events c
|
|
19123
|
+
WHERE c.root_session_id = audit_events.id
|
|
19124
|
+
AND c.event_type NOT IN ('hook', 'config_scan'))`;
|
|
18925
19125
|
var SqliteActivityRepository = class {
|
|
18926
19126
|
constructor(db, now = () => Date.now()) {
|
|
18927
19127
|
this.db = db;
|
|
@@ -18935,7 +19135,8 @@ var SqliteActivityRepository = class {
|
|
|
18935
19135
|
const sessionsToday = countScalar(
|
|
18936
19136
|
this.db,
|
|
18937
19137
|
`SELECT count(*) AS n FROM audit_events
|
|
18938
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
19138
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
|
|
19139
|
+
AND ${HAS_ACTIVITY}`,
|
|
18939
19140
|
[startMs, endMs]
|
|
18940
19141
|
);
|
|
18941
19142
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
@@ -19007,6 +19208,13 @@ var SqliteActivityRepository = class {
|
|
|
19007
19208
|
);
|
|
19008
19209
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
19009
19210
|
}
|
|
19211
|
+
const emptyCount = countScalar(
|
|
19212
|
+
this.db,
|
|
19213
|
+
`SELECT count(*) AS n FROM audit_events
|
|
19214
|
+
WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
|
|
19215
|
+
params
|
|
19216
|
+
);
|
|
19217
|
+
if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
|
|
19010
19218
|
if (cursor) {
|
|
19011
19219
|
conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
|
|
19012
19220
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
@@ -19043,7 +19251,7 @@ var SqliteActivityRepository = class {
|
|
|
19043
19251
|
);
|
|
19044
19252
|
const last = page[page.length - 1];
|
|
19045
19253
|
const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
|
|
19046
|
-
return Promise.resolve({ items, nextCursor });
|
|
19254
|
+
return Promise.resolve({ items, nextCursor, emptyCount });
|
|
19047
19255
|
}
|
|
19048
19256
|
getSession(sessionId) {
|
|
19049
19257
|
const rootRow = getRow(
|
|
@@ -20349,9 +20557,43 @@ var SqliteFindingsRepository = class {
|
|
|
20349
20557
|
}))
|
|
20350
20558
|
);
|
|
20351
20559
|
}
|
|
20560
|
+
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
20561
|
+
* session-stamped events (served by idx_events_session_id), so the Activity
|
|
20562
|
+
* page can label its findings link without the grouped pipeline. */
|
|
20563
|
+
sessionFindingsCount(sessionId) {
|
|
20564
|
+
if (!sessionId) return Promise.resolve(0);
|
|
20565
|
+
return Promise.resolve(
|
|
20566
|
+
countScalar(
|
|
20567
|
+
this.db,
|
|
20568
|
+
`SELECT count(*) AS n FROM findings f
|
|
20569
|
+
JOIN events e ON e.id = f.event_id
|
|
20570
|
+
WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
|
|
20571
|
+
{ sessionId }
|
|
20572
|
+
)
|
|
20573
|
+
);
|
|
20574
|
+
}
|
|
20575
|
+
/** Per-rule transcript firing tally for one session — reads the OTHER finding
|
|
20576
|
+
* store (inspection_findings, keyed to audit_events): every detection the
|
|
20577
|
+
* transcript pass recorded, counted per firing rather than per unique value.
|
|
20578
|
+
* Rides on session-scoped grouped responses so the findings view can
|
|
20579
|
+
* reconcile the Activity page's tally with the deduped groups it lists. */
|
|
20580
|
+
sessionFirings(sessionId) {
|
|
20581
|
+
return Object.fromEntries(
|
|
20582
|
+
countBy(
|
|
20583
|
+
this.db,
|
|
20584
|
+
`SELECT d.rule_id AS k, count(*) AS n
|
|
20585
|
+
FROM inspection_findings f
|
|
20586
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20587
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20588
|
+
WHERE e.root_session_id = :sessionId
|
|
20589
|
+
GROUP BY d.rule_id`,
|
|
20590
|
+
{ sessionId }
|
|
20591
|
+
)
|
|
20592
|
+
);
|
|
20593
|
+
}
|
|
20352
20594
|
/**
|
|
20353
|
-
* Grouped findings for the dashboard — joins findings⋈events (repo/file
|
|
20354
|
-
* from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
20595
|
+
* Grouped findings for the dashboard — joins findings⋈events (repo/file/
|
|
20596
|
+
* toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
20355
20597
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
20356
20598
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
20357
20599
|
* reflect the full filtered set; `items` is the requested
|
|
@@ -20368,11 +20610,16 @@ var SqliteFindingsRepository = class {
|
|
|
20368
20610
|
* rule is ever restated in SQL.
|
|
20369
20611
|
*/
|
|
20370
20612
|
listGroupedFindings(query) {
|
|
20371
|
-
const
|
|
20613
|
+
const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
|
|
20614
|
+
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
20615
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
20616
|
+
predicate: sessionPredicate,
|
|
20617
|
+
params: sessionParams
|
|
20618
|
+
});
|
|
20372
20619
|
const rows = allRows(
|
|
20373
20620
|
this.db.prepare(
|
|
20374
20621
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
20375
|
-
occurred_at, source_tool, repo, file, kind, finding_key, latest_status
|
|
20622
|
+
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
20376
20623
|
FROM (
|
|
20377
20624
|
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
20378
20625
|
f.severity AS severity, f.masked_match AS masked_match,
|
|
@@ -20380,6 +20627,7 @@ var SqliteFindingsRepository = class {
|
|
|
20380
20627
|
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
20381
20628
|
json_extract(e.metadata, '$.repo') AS repo,
|
|
20382
20629
|
json_extract(e.metadata, '$.filePath') AS file,
|
|
20630
|
+
json_extract(e.metadata, '$.toolName') AS tool_name,
|
|
20383
20631
|
e.kind AS kind, f.finding_key AS finding_key,
|
|
20384
20632
|
latest.status AS latest_status,
|
|
20385
20633
|
ROW_NUMBER() OVER (
|
|
@@ -20390,11 +20638,12 @@ var SqliteFindingsRepository = class {
|
|
|
20390
20638
|
JOIN events e ON e.id = f.event_id
|
|
20391
20639
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20392
20640
|
ON latest.finding_key = f.finding_key
|
|
20641
|
+
${sessionPredicate}
|
|
20393
20642
|
)
|
|
20394
20643
|
WHERE rn <= :cap
|
|
20395
20644
|
ORDER BY occurred_at DESC, id DESC`
|
|
20396
20645
|
),
|
|
20397
|
-
{ cap: PREVIEW_INSTANCES_PER_GROUP }
|
|
20646
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
|
|
20398
20647
|
);
|
|
20399
20648
|
const groupable = rows.map((r) => ({
|
|
20400
20649
|
id: r.id,
|
|
@@ -20408,6 +20657,7 @@ var SqliteFindingsRepository = class {
|
|
|
20408
20657
|
sourceTool: r.source_tool,
|
|
20409
20658
|
repo: r.repo ?? "",
|
|
20410
20659
|
file: r.file ?? "",
|
|
20660
|
+
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
20411
20661
|
status: deriveInstanceStatus(r)
|
|
20412
20662
|
}));
|
|
20413
20663
|
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
@@ -20426,7 +20676,13 @@ var SqliteFindingsRepository = class {
|
|
|
20426
20676
|
};
|
|
20427
20677
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
20428
20678
|
const items = sorted.slice(0, limit);
|
|
20429
|
-
return Promise.resolve({
|
|
20679
|
+
return Promise.resolve({
|
|
20680
|
+
totals,
|
|
20681
|
+
facets,
|
|
20682
|
+
items,
|
|
20683
|
+
nextCursor: null,
|
|
20684
|
+
...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
|
|
20685
|
+
});
|
|
20430
20686
|
}
|
|
20431
20687
|
/**
|
|
20432
20688
|
* One row per rule_id, folding EVERY instance of the group into the values
|
|
@@ -20450,9 +20706,10 @@ var SqliteFindingsRepository = class {
|
|
|
20450
20706
|
* would silently lose, so it is fetched only when the request actually
|
|
20451
20707
|
* carries a `q`.
|
|
20452
20708
|
*/
|
|
20453
|
-
groupAggregates(withSearchText) {
|
|
20709
|
+
groupAggregates(withSearchText, scope) {
|
|
20454
20710
|
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
20455
|
-
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files
|
|
20711
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
|
|
20712
|
+
group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
20456
20713
|
const rows = this.db.prepare(
|
|
20457
20714
|
`SELECT f.rule_id AS rule_id,
|
|
20458
20715
|
count(*) AS instance_count,
|
|
@@ -20469,8 +20726,9 @@ var SqliteFindingsRepository = class {
|
|
|
20469
20726
|
JOIN events e ON e.id = f.event_id
|
|
20470
20727
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20471
20728
|
ON latest.finding_key = f.finding_key
|
|
20729
|
+
${scope.predicate}
|
|
20472
20730
|
GROUP BY f.rule_id`
|
|
20473
|
-
).all();
|
|
20731
|
+
).all(scope.params);
|
|
20474
20732
|
return new Map(
|
|
20475
20733
|
rows.map((r) => [
|
|
20476
20734
|
r.rule_id,
|
|
@@ -20494,7 +20752,9 @@ var SqliteFindingsRepository = class {
|
|
|
20494
20752
|
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
20495
20753
|
// tell "no q this request" from "a group with no repo/file at all"
|
|
20496
20754
|
// and skip priming a haystack nothing will read.
|
|
20497
|
-
...withSearchText ? {
|
|
20755
|
+
...withSearchText ? {
|
|
20756
|
+
searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
|
|
20757
|
+
} : {}
|
|
20498
20758
|
}
|
|
20499
20759
|
])
|
|
20500
20760
|
);
|
|
@@ -22230,7 +22490,6 @@ var SqliteScanLedgerRepository = class {
|
|
|
22230
22490
|
// ../packages/persistence/src/repositories/security.ts
|
|
22231
22491
|
var DAY_MS4 = 864e5;
|
|
22232
22492
|
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
22233
|
-
var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
|
|
22234
22493
|
var ACTION_TO_KIND = {
|
|
22235
22494
|
block: "blocked",
|
|
22236
22495
|
redact: "redacted",
|
|
@@ -22245,8 +22504,14 @@ var SCAN_COVERAGE = [
|
|
|
22245
22504
|
{ provider: "copilot", coverage: 0, supported: false },
|
|
22246
22505
|
{ provider: "api", coverage: 0, supported: false }
|
|
22247
22506
|
];
|
|
22507
|
+
var GRANULARITY = {
|
|
22508
|
+
"7d": "day",
|
|
22509
|
+
"30d": "day",
|
|
22510
|
+
"3m": "week",
|
|
22511
|
+
"6m": "week"
|
|
22512
|
+
};
|
|
22248
22513
|
function granularityFor(range) {
|
|
22249
|
-
return range
|
|
22514
|
+
return GRANULARITY[range];
|
|
22250
22515
|
}
|
|
22251
22516
|
function startOfUtcDay2(ms) {
|
|
22252
22517
|
return Math.floor(ms / DAY_MS4) * DAY_MS4;
|
|
@@ -23968,7 +24233,6 @@ var db_table_name_default = {
|
|
|
23968
24233
|
"SELECT * FROM ",
|
|
23969
24234
|
"SELECT COUNT(*) FROM ",
|
|
23970
24235
|
"INSERT INTO ",
|
|
23971
|
-
"UPDATE ",
|
|
23972
24236
|
"DELETE FROM ",
|
|
23973
24237
|
"CREATE TABLE ",
|
|
23974
24238
|
"ALTER TABLE ",
|
|
@@ -25709,6 +25973,26 @@ function recordProjectInventory(db, target) {
|
|
|
25709
25973
|
}
|
|
25710
25974
|
}
|
|
25711
25975
|
|
|
25976
|
+
// ../packages/local-ops/src/self-exec.ts
|
|
25977
|
+
import { createRequire } from "module";
|
|
25978
|
+
var require2 = createRequire(import.meta.url);
|
|
25979
|
+
function isSea() {
|
|
25980
|
+
try {
|
|
25981
|
+
const sea = require2("node:sea");
|
|
25982
|
+
return typeof sea.isSea === "function" ? sea.isSea() : false;
|
|
25983
|
+
} catch {
|
|
25984
|
+
return false;
|
|
25985
|
+
}
|
|
25986
|
+
}
|
|
25987
|
+
function buildReinvocation(sea, execPath, entry, subcommand, extraArgs) {
|
|
25988
|
+
if (sea) return { command: execPath, args: [subcommand, ...extraArgs] };
|
|
25989
|
+
if (entry === void 0) return null;
|
|
25990
|
+
return { command: execPath, args: [entry, subcommand, ...extraArgs] };
|
|
25991
|
+
}
|
|
25992
|
+
function reinvokeArgv(subcommand, extraArgs = []) {
|
|
25993
|
+
return buildReinvocation(isSea(), process.execPath, process.argv[1], subcommand, extraArgs);
|
|
25994
|
+
}
|
|
25995
|
+
|
|
25712
25996
|
// ../packages/local-ops/src/update-cache.ts
|
|
25713
25997
|
import { spawn } from "child_process";
|
|
25714
25998
|
import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
@@ -25754,10 +26038,10 @@ function refreshCache(home) {
|
|
|
25754
26038
|
});
|
|
25755
26039
|
}
|
|
25756
26040
|
function triggerBackgroundRefresh(home) {
|
|
25757
|
-
const
|
|
25758
|
-
if (!
|
|
26041
|
+
const reinvoke = reinvokeArgv("__update-refresh", ["--home", home]);
|
|
26042
|
+
if (!reinvoke) return;
|
|
25759
26043
|
try {
|
|
25760
|
-
spawn(
|
|
26044
|
+
spawn(reinvoke.command, reinvoke.args, {
|
|
25761
26045
|
detached: true,
|
|
25762
26046
|
stdio: "ignore"
|
|
25763
26047
|
}).unref();
|
|
@@ -26080,10 +26364,10 @@ function runCompletion(argv) {
|
|
|
26080
26364
|
// src/commands/dashboard.ts
|
|
26081
26365
|
import { spawn as spawn3 } from "child_process";
|
|
26082
26366
|
import { existsSync as existsSync8 } from "fs";
|
|
26083
|
-
import { createRequire } from "module";
|
|
26367
|
+
import { createRequire as createRequire2 } from "module";
|
|
26084
26368
|
import { createServer } from "net";
|
|
26085
26369
|
import { dirname as dirname3, join as join14 } from "path";
|
|
26086
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
26370
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
26087
26371
|
import { parseArgs as parseArgs2 } from "util";
|
|
26088
26372
|
|
|
26089
26373
|
// src/lib/open-url.ts
|
|
@@ -26099,8 +26383,11 @@ function openUrl(url2) {
|
|
|
26099
26383
|
}
|
|
26100
26384
|
|
|
26101
26385
|
// src/commands/dashboard.ts
|
|
26102
|
-
var
|
|
26386
|
+
var require3 = createRequire2(import.meta.url);
|
|
26103
26387
|
var here = dirname3(fileURLToPath2(import.meta.url));
|
|
26388
|
+
function pkgBase() {
|
|
26389
|
+
return isSea() ? dirname3(process.execPath) : join14(here, "..");
|
|
26390
|
+
}
|
|
26104
26391
|
function isPortFree(port) {
|
|
26105
26392
|
return new Promise((resolve2) => {
|
|
26106
26393
|
const probe = createServer();
|
|
@@ -26151,16 +26438,17 @@ async function runDashboard(argv) {
|
|
|
26151
26438
|
return;
|
|
26152
26439
|
}
|
|
26153
26440
|
refreshUpdateMirror(dataDir());
|
|
26441
|
+
const base = pkgBase();
|
|
26154
26442
|
const bundledServer = [
|
|
26155
|
-
join14(
|
|
26156
|
-
join14(
|
|
26443
|
+
join14(base, "web-ui", "server.js"),
|
|
26444
|
+
join14(base, "web-ui", "web-ui", "server.js")
|
|
26157
26445
|
].find((p) => existsSync8(p));
|
|
26158
26446
|
if (bundledServer) {
|
|
26159
26447
|
launchStandalone(bundledServer, port, shouldOpen, url2);
|
|
26160
26448
|
return;
|
|
26161
26449
|
}
|
|
26162
|
-
const webUiDir = dirname3(
|
|
26163
|
-
const nextBin =
|
|
26450
|
+
const webUiDir = dirname3(require3.resolve("@akasecurity/web-ui/package.json"));
|
|
26451
|
+
const nextBin = require3.resolve("next/dist/bin/next", { paths: [webUiDir] });
|
|
26164
26452
|
const startNext = () => {
|
|
26165
26453
|
const child = spawn3(
|
|
26166
26454
|
process.execPath,
|
|
@@ -26189,7 +26477,13 @@ async function runDashboard(argv) {
|
|
|
26189
26477
|
});
|
|
26190
26478
|
}
|
|
26191
26479
|
function launchStandalone(serverJs, port, shouldOpen, url2) {
|
|
26192
|
-
const
|
|
26480
|
+
const reinvoke = reinvokeArgv("__dashboard-server", ["--server-js", serverJs]);
|
|
26481
|
+
if (!reinvoke) {
|
|
26482
|
+
process.stderr.write("aka dashboard: cannot resolve the CLI entry to launch the server.\n");
|
|
26483
|
+
process.exitCode = 1;
|
|
26484
|
+
return;
|
|
26485
|
+
}
|
|
26486
|
+
const child = spawn3(reinvoke.command, reinvoke.args, {
|
|
26193
26487
|
cwd: dirname3(serverJs),
|
|
26194
26488
|
stdio: ["inherit", "pipe", "inherit"],
|
|
26195
26489
|
// Inherit the parent env so the child server sees PATH etc.; PORT/HOSTNAME tell
|
|
@@ -26200,6 +26494,16 @@ function launchStandalone(serverJs, port, shouldOpen, url2) {
|
|
|
26200
26494
|
onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
|
|
26201
26495
|
`);
|
|
26202
26496
|
}
|
|
26497
|
+
async function runDashboardServer(argv) {
|
|
26498
|
+
const { values } = parseArgs2({ args: argv, options: { "server-js": { type: "string" } } });
|
|
26499
|
+
const serverJs = values["server-js"];
|
|
26500
|
+
if (serverJs === void 0 || serverJs === "") {
|
|
26501
|
+
process.stderr.write("aka __dashboard-server: --server-js <path> is required\n");
|
|
26502
|
+
process.exitCode = 1;
|
|
26503
|
+
return;
|
|
26504
|
+
}
|
|
26505
|
+
await import(pathToFileURL(serverJs).href);
|
|
26506
|
+
}
|
|
26203
26507
|
function onReadyOpen(child, shouldOpen, url2, banner) {
|
|
26204
26508
|
process.stdout.write(banner);
|
|
26205
26509
|
let opened = false;
|
|
@@ -27214,6 +27518,7 @@ Then run \`aka init\` to set up the local store.
|
|
|
27214
27518
|
}
|
|
27215
27519
|
|
|
27216
27520
|
// src/commands/init.ts
|
|
27521
|
+
var PLUGIN_OFFER_IDENTITY = `${PRODUCT_NAME} \u2014 ${PRODUCT_TAGLINE}`;
|
|
27217
27522
|
async function runInit(argv) {
|
|
27218
27523
|
const { values } = parseArgs6({
|
|
27219
27524
|
args: argv,
|
|
@@ -27265,6 +27570,7 @@ async function offerPluginInstall(autoYes) {
|
|
|
27265
27570
|
if (!process.stdin.isTTY) {
|
|
27266
27571
|
out.write(
|
|
27267
27572
|
`
|
|
27573
|
+
${PLUGIN_OFFER_IDENTITY}
|
|
27268
27574
|
No Claude Code plugin detected. Install it via the marketplace:
|
|
27269
27575
|
/plugin marketplace add ${agent.marketplaceSource ?? ""}
|
|
27270
27576
|
/plugin install ${ref}
|
|
@@ -27273,8 +27579,11 @@ Or re-run \`aka init --yes\` to install it automatically.
|
|
|
27273
27579
|
);
|
|
27274
27580
|
return;
|
|
27275
27581
|
}
|
|
27276
|
-
if (!await confirm(
|
|
27277
|
-
|
|
27582
|
+
if (!await confirm(
|
|
27583
|
+
`
|
|
27584
|
+
${PLUGIN_OFFER_IDENTITY}
|
|
27585
|
+
Install the ${PRODUCT_NAME} plugin for Claude Code now (via the marketplace)? [y/N] `
|
|
27586
|
+
)) {
|
|
27278
27587
|
out.write("Skipped. Install anytime with `aka plugins install claude-code`.\n");
|
|
27279
27588
|
return;
|
|
27280
27589
|
}
|
|
@@ -27397,19 +27706,15 @@ function runScan(argv) {
|
|
|
27397
27706
|
// src/commands/stats.ts
|
|
27398
27707
|
import { parseArgs as parseArgs8 } from "util";
|
|
27399
27708
|
var DAY_MS5 = 864e5;
|
|
27400
|
-
var RANGE_DAYS2 = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
|
|
27401
|
-
function parseRange(value) {
|
|
27402
|
-
return SECURITY_RANGES.includes(value ?? "") ? value : "30d";
|
|
27403
|
-
}
|
|
27404
27709
|
async function runStats(argv) {
|
|
27405
27710
|
const { values } = parseArgs8({
|
|
27406
27711
|
args: argv,
|
|
27407
27712
|
options: { ...HOME_OPTION, range: { type: "string" } }
|
|
27408
27713
|
});
|
|
27409
27714
|
const home = homeBase(values.home);
|
|
27410
|
-
const range =
|
|
27715
|
+
const range = parseTimeRange(values.range);
|
|
27411
27716
|
const db = openLocalDatabase(dataDir(home));
|
|
27412
|
-
const tokenFromMs = Date.now() -
|
|
27717
|
+
const tokenFromMs = Date.now() - RANGE_DAYS[range] * DAY_MS5;
|
|
27413
27718
|
const [health, severity, enforcement, packs2, recent, tokenReports] = await Promise.all([
|
|
27414
27719
|
db.findings.healthSummary(),
|
|
27415
27720
|
db.security.severitySummary(),
|
|
@@ -27432,7 +27737,7 @@ async function runStats(argv) {
|
|
|
27432
27737
|
Enforcement (${range}): ${String(enforcement.total)} intercepted
|
|
27433
27738
|
`);
|
|
27434
27739
|
for (const a of enforcement.actions) {
|
|
27435
|
-
const delta = a.delta === 0 ? "" : ` (${a.delta > 0 ? "+" : ""}${String(a.delta)}
|
|
27740
|
+
const delta = a.delta === 0 ? "" : ` (${a.delta > 0 ? "+" : ""}${String(a.delta)} vs prior ${range})`;
|
|
27436
27741
|
out.write(` ${a.kind.padEnd(9)} ${String(a.count)}${delta}
|
|
27437
27742
|
`);
|
|
27438
27743
|
}
|
|
@@ -28249,9 +28554,19 @@ var COMMANDS = {
|
|
|
28249
28554
|
// Hidden: the detached background refresh spawned by the passive update notice.
|
|
28250
28555
|
"__update-refresh": (argv) => {
|
|
28251
28556
|
refreshCache(homeFromArgv(argv));
|
|
28252
|
-
}
|
|
28253
|
-
|
|
28254
|
-
|
|
28557
|
+
},
|
|
28558
|
+
// Hidden: boots the bundled Next standalone server in-process (see dashboard.ts).
|
|
28559
|
+
// `aka dashboard` spawns this so a SEA binary can serve the dashboard without
|
|
28560
|
+
// exec-ing an external script. Returns the promise so main() awaits the boot.
|
|
28561
|
+
"__dashboard-server": (argv) => runDashboardServer(argv)
|
|
28562
|
+
};
|
|
28563
|
+
var SKIP_NOTICE = /* @__PURE__ */ new Set([
|
|
28564
|
+
"update",
|
|
28565
|
+
"check-updates",
|
|
28566
|
+
"__update-refresh",
|
|
28567
|
+
"__dashboard-server",
|
|
28568
|
+
"completion"
|
|
28569
|
+
]);
|
|
28255
28570
|
var USAGE = `aka \u2014 AI Traffic Control (local-first, everything stays on your machine)
|
|
28256
28571
|
|
|
28257
28572
|
Usage: aka <command> [options]
|