lockpicks 0.3.0 → 0.4.0
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 +11 -2
- package/dist/lockpick.mjs +1564 -151
- package/package.json +13 -12
package/dist/lockpick.mjs
CHANGED
|
@@ -6683,8 +6683,8 @@ var require_formats = __commonJS({
|
|
|
6683
6683
|
return false;
|
|
6684
6684
|
const year = +matches[1];
|
|
6685
6685
|
const month = +matches[2];
|
|
6686
|
-
const
|
|
6687
|
-
return month >= 1 && month <= 12 &&
|
|
6686
|
+
const day2 = +matches[3];
|
|
6687
|
+
return month >= 1 && month <= 12 && day2 >= 1 && day2 <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]);
|
|
6688
6688
|
}
|
|
6689
6689
|
function compareDate(d1, d2) {
|
|
6690
6690
|
if (!(d1 && d2))
|
|
@@ -7386,7 +7386,8 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
7386
7386
|
|
|
7387
7387
|
// src/commands.ts
|
|
7388
7388
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
7389
|
-
import { readFile as readFile5, writeFile as writeFile2 } from "node:fs/promises";
|
|
7389
|
+
import { access as access2, mkdir as mkdir4, readFile as readFile5, writeFile as writeFile2 } from "node:fs/promises";
|
|
7390
|
+
import { dirname as dirname5 } from "node:path";
|
|
7390
7391
|
import { hostname as hostname3 } from "node:os";
|
|
7391
7392
|
|
|
7392
7393
|
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.1_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
@@ -11497,12 +11498,12 @@ var $ZodPipe = /* @__PURE__ */ $constructor("$ZodPipe", (inst, def) => {
|
|
|
11497
11498
|
return handlePipeResult(left, def.out, ctx);
|
|
11498
11499
|
};
|
|
11499
11500
|
});
|
|
11500
|
-
function handlePipeResult(left,
|
|
11501
|
+
function handlePipeResult(left, next2, ctx) {
|
|
11501
11502
|
if (left.issues.length) {
|
|
11502
11503
|
left.aborted = true;
|
|
11503
11504
|
return left;
|
|
11504
11505
|
}
|
|
11505
|
-
return
|
|
11506
|
+
return next2._zod.run({ value: left.value, issues: left.issues, fallback: left.fallback }, ctx);
|
|
11506
11507
|
}
|
|
11507
11508
|
var $ZodCodec = /* @__PURE__ */ $constructor("$ZodCodec", (inst, def) => {
|
|
11508
11509
|
$ZodType.init(inst, def);
|
|
@@ -23580,6 +23581,74 @@ function readBriefCommand(agent2, file2 = FIX_ALL_FILE) {
|
|
|
23580
23581
|
);
|
|
23581
23582
|
}
|
|
23582
23583
|
|
|
23584
|
+
// ../../packages/contracts/src/run-outcome.ts
|
|
23585
|
+
var RUN_OUTCOME_LEVELS = [
|
|
23586
|
+
"full",
|
|
23587
|
+
"headers-only",
|
|
23588
|
+
"blocked",
|
|
23589
|
+
"transport",
|
|
23590
|
+
"unreachable",
|
|
23591
|
+
"terminal-failure"
|
|
23592
|
+
];
|
|
23593
|
+
var runOutcomeLevelSchema = external_exports.enum(RUN_OUTCOME_LEVELS);
|
|
23594
|
+
var RUN_OUTCOME_COVERAGE = [
|
|
23595
|
+
"page",
|
|
23596
|
+
"headers-and-records",
|
|
23597
|
+
"records-only",
|
|
23598
|
+
"none"
|
|
23599
|
+
];
|
|
23600
|
+
var runOutcomeCoverageSchema = external_exports.enum(RUN_OUTCOME_COVERAGE);
|
|
23601
|
+
var RUN_CHALLENGE_VENDORS = ["cloudflare", "vercel", "other"];
|
|
23602
|
+
var runChallengeVendorSchema = external_exports.enum(RUN_CHALLENGE_VENDORS);
|
|
23603
|
+
var RUN_OUTCOME_REASONS = [
|
|
23604
|
+
"no-address",
|
|
23605
|
+
"dns-error",
|
|
23606
|
+
"private-address",
|
|
23607
|
+
"refused",
|
|
23608
|
+
"no-answer",
|
|
23609
|
+
"connection-closed",
|
|
23610
|
+
"certificate",
|
|
23611
|
+
"old-tls",
|
|
23612
|
+
"large-headers",
|
|
23613
|
+
"invalid-response",
|
|
23614
|
+
"www",
|
|
23615
|
+
"other-site",
|
|
23616
|
+
"sign-in",
|
|
23617
|
+
"redirect-loop",
|
|
23618
|
+
"redirect",
|
|
23619
|
+
"bot-check",
|
|
23620
|
+
"denied",
|
|
23621
|
+
"rate-limited",
|
|
23622
|
+
"not-found",
|
|
23623
|
+
"gone",
|
|
23624
|
+
"client-error",
|
|
23625
|
+
"server-error",
|
|
23626
|
+
"maintenance",
|
|
23627
|
+
"parked",
|
|
23628
|
+
"empty",
|
|
23629
|
+
"not-html",
|
|
23630
|
+
"app-shell",
|
|
23631
|
+
"interrupted",
|
|
23632
|
+
"not-delivered"
|
|
23633
|
+
];
|
|
23634
|
+
var runOutcomeReasonSchema = external_exports.enum(RUN_OUTCOME_REASONS);
|
|
23635
|
+
var publicRunOutcomeSchema = external_exports.object({
|
|
23636
|
+
level: runOutcomeLevelSchema,
|
|
23637
|
+
coverage: runOutcomeCoverageSchema,
|
|
23638
|
+
reason: runOutcomeReasonSchema.optional(),
|
|
23639
|
+
vendor: runChallengeVendorSchema.optional(),
|
|
23640
|
+
/** HTTP status the homepage answered with, when one arrived. */
|
|
23641
|
+
homepageStatus: external_exports.number().int().min(100).max(599).optional(),
|
|
23642
|
+
/** Where a www-twin or cross-site redirect pointed, to check instead. */
|
|
23643
|
+
redirectOrigin: external_exports.string().max(300).optional(),
|
|
23644
|
+
/**
|
|
23645
|
+
* Terminal failures only: whether the run had contacted the site before it
|
|
23646
|
+
* stopped. A run is charged at its first contact, so false also means it
|
|
23647
|
+
* did not use one of the account's runs.
|
|
23648
|
+
*/
|
|
23649
|
+
contacted: external_exports.boolean().optional()
|
|
23650
|
+
});
|
|
23651
|
+
|
|
23583
23652
|
// ../../packages/contracts/src/quick-check.ts
|
|
23584
23653
|
var QUICK_CHECK_IDS = [
|
|
23585
23654
|
"https",
|
|
@@ -23640,6 +23709,7 @@ var QUICK_STACK = [
|
|
|
23640
23709
|
];
|
|
23641
23710
|
var quickStackSchema = external_exports.enum(QUICK_STACK);
|
|
23642
23711
|
var QUICK_STACK_MAX = 8;
|
|
23712
|
+
var PULSE_STACK_MAX = 12;
|
|
23643
23713
|
var QUICK_CHECK_MAX_REQUESTS = 6;
|
|
23644
23714
|
var quickCheckFailureSchema = external_exports.enum([
|
|
23645
23715
|
"unreachable",
|
|
@@ -23649,7 +23719,9 @@ var quickCheckFailureSchema = external_exports.enum([
|
|
|
23649
23719
|
"unsupported-response",
|
|
23650
23720
|
"timeout",
|
|
23651
23721
|
"budget",
|
|
23652
|
-
"unavailable"
|
|
23722
|
+
"unavailable",
|
|
23723
|
+
/** A bot check answered instead of the homepage (any status, 200 included). */
|
|
23724
|
+
"bot-check"
|
|
23653
23725
|
]);
|
|
23654
23726
|
var severitySchema = external_exports.enum(["high", "medium", "low", "info"]);
|
|
23655
23727
|
var statusSchema = external_exports.enum(["pass", "attention", "unknown"]);
|
|
@@ -23664,6 +23736,8 @@ var quickCheckSummarySchema = external_exports.object({
|
|
|
23664
23736
|
outcome: external_exports.enum(["complete", "partial", "failed"]),
|
|
23665
23737
|
failure: quickCheckFailureSchema.optional(),
|
|
23666
23738
|
httpStatus: external_exports.int().min(100).max(599).optional(),
|
|
23739
|
+
/** With `bot-check`: whose bot check answered. */
|
|
23740
|
+
challengeVendor: runChallengeVendorSchema.optional(),
|
|
23667
23741
|
redirectOrigin: external_exports.url().max(300).optional(),
|
|
23668
23742
|
/** The www twin the homepage redirected to; the checks describe it. */
|
|
23669
23743
|
finalOrigin: external_exports.url().max(300).optional(),
|
|
@@ -23701,6 +23775,8 @@ var quickCheckTldrSchema = external_exports.object({
|
|
|
23701
23775
|
status: external_exports.enum(["pending", "complete", "partial", "failed"]),
|
|
23702
23776
|
failure: quickCheckFailureSchema.optional(),
|
|
23703
23777
|
httpStatus: external_exports.int().min(100).max(599).optional(),
|
|
23778
|
+
/** With `bot-check`: whose bot check answered. */
|
|
23779
|
+
challengeVendor: runChallengeVendorSchema.optional(),
|
|
23704
23780
|
redirectOrigin: external_exports.url().max(300).optional(),
|
|
23705
23781
|
worthFixing: external_exports.int().nonnegative(),
|
|
23706
23782
|
notes: external_exports.int().nonnegative(),
|
|
@@ -23736,6 +23812,25 @@ var quickCheckTldrSchema = external_exports.object({
|
|
|
23736
23812
|
expiresAt: external_exports.iso.datetime()
|
|
23737
23813
|
}).strict();
|
|
23738
23814
|
|
|
23815
|
+
// ../../packages/contracts/src/scan-standards.ts
|
|
23816
|
+
var standardsReferenceSchema = external_exports.object({
|
|
23817
|
+
owasp: external_exports.array(external_exports.string().regex(/^A(?:0[1-9]|10):2025$/)).max(10),
|
|
23818
|
+
cwe: external_exports.array(external_exports.string().regex(/^CWE-\d+$/)).max(20),
|
|
23819
|
+
asvs: external_exports.array(external_exports.string().regex(/^v5\.0\.0-\d+\.\d+\.\d+$/)).max(20),
|
|
23820
|
+
wstg: external_exports.array(external_exports.string().regex(/^WSTG-v42-[A-Z]+-\d+$/)).max(20),
|
|
23821
|
+
relationship: external_exports.literal("related-control")
|
|
23822
|
+
}).strict();
|
|
23823
|
+
var standardsSummarySchema = external_exports.object({
|
|
23824
|
+
wstgVersion: external_exports.literal("4.2"),
|
|
23825
|
+
owaspVersion: external_exports.literal("2025"),
|
|
23826
|
+
asvsVersion: external_exports.literal("5.0.0"),
|
|
23827
|
+
mappingVersion: external_exports.literal("1"),
|
|
23828
|
+
levelOneRequirements: external_exports.number().int().nonnegative(),
|
|
23829
|
+
relatedLevelOneRequirements: external_exports.number().int().nonnegative(),
|
|
23830
|
+
verifiedLevelOneRequirements: external_exports.literal(0),
|
|
23831
|
+
unverifiedLevelOneRequirements: external_exports.number().int().nonnegative()
|
|
23832
|
+
}).strict();
|
|
23833
|
+
|
|
23739
23834
|
// ../../packages/contracts/src/finding-triage.ts
|
|
23740
23835
|
var FINDING_VERDICTS = [
|
|
23741
23836
|
"confirmed",
|
|
@@ -23774,6 +23869,510 @@ var findingTriageSchema = external_exports.object({
|
|
|
23774
23869
|
message: "A not_actionable verdict needs positive defeating evidence."
|
|
23775
23870
|
});
|
|
23776
23871
|
|
|
23872
|
+
// ../../packages/contracts/src/public-pulse.ts
|
|
23873
|
+
var explanationSchema = external_exports.object({
|
|
23874
|
+
title: external_exports.string(),
|
|
23875
|
+
meaning: external_exports.string(),
|
|
23876
|
+
fix: external_exports.string()
|
|
23877
|
+
});
|
|
23878
|
+
var evidenceClassSchema = external_exports.enum([
|
|
23879
|
+
"observed",
|
|
23880
|
+
"suspected",
|
|
23881
|
+
"informational"
|
|
23882
|
+
]);
|
|
23883
|
+
var publicCoverageAreaSchema = external_exports.enum([
|
|
23884
|
+
"html",
|
|
23885
|
+
"scripts",
|
|
23886
|
+
"maps",
|
|
23887
|
+
"exposure",
|
|
23888
|
+
"cors",
|
|
23889
|
+
"tls",
|
|
23890
|
+
"validation",
|
|
23891
|
+
"components",
|
|
23892
|
+
"security-policy",
|
|
23893
|
+
"domain",
|
|
23894
|
+
"sitemap"
|
|
23895
|
+
]);
|
|
23896
|
+
var assessmentEvidenceSchema = external_exports.object({
|
|
23897
|
+
version: external_exports.literal("assessment-evidence@1"),
|
|
23898
|
+
observations: external_exports.number().int().nonnegative(),
|
|
23899
|
+
suspected: external_exports.number().int().nonnegative(),
|
|
23900
|
+
informational: external_exports.number().int().nonnegative(),
|
|
23901
|
+
unassessedChecks: external_exports.number().int().nonnegative(),
|
|
23902
|
+
areas: external_exports.array(
|
|
23903
|
+
external_exports.object({
|
|
23904
|
+
area: publicCoverageAreaSchema,
|
|
23905
|
+
state: external_exports.enum(["complete", "limited", "not-run", "unknown"]),
|
|
23906
|
+
reason: external_exports.enum([
|
|
23907
|
+
"sampled",
|
|
23908
|
+
"incomplete",
|
|
23909
|
+
"not-assessed",
|
|
23910
|
+
"historical-unavailable"
|
|
23911
|
+
])
|
|
23912
|
+
})
|
|
23913
|
+
)
|
|
23914
|
+
});
|
|
23915
|
+
var publicCheckSchema = external_exports.object({
|
|
23916
|
+
standards: standardsReferenceSchema.optional(),
|
|
23917
|
+
id: external_exports.string(),
|
|
23918
|
+
category: external_exports.string(),
|
|
23919
|
+
status: external_exports.enum(["pass", "attention", "unknown"]),
|
|
23920
|
+
severity: external_exports.enum(["medium", "low", "info"]),
|
|
23921
|
+
plain: explanationSchema,
|
|
23922
|
+
technical: explanationSchema,
|
|
23923
|
+
evidence: external_exports.string(),
|
|
23924
|
+
affected: external_exports.array(external_exports.string()),
|
|
23925
|
+
prompt: external_exports.string(),
|
|
23926
|
+
triage: findingTriageSchema.optional()
|
|
23927
|
+
});
|
|
23928
|
+
var publicFindingSchema = external_exports.object({
|
|
23929
|
+
standards: standardsReferenceSchema.optional(),
|
|
23930
|
+
id: external_exports.string(),
|
|
23931
|
+
kind: external_exports.string(),
|
|
23932
|
+
severity: external_exports.enum(["high", "medium", "low", "info"]),
|
|
23933
|
+
confidence: external_exports.enum(["observed", "potential"]),
|
|
23934
|
+
evidenceClass: evidenceClassSchema.optional(),
|
|
23935
|
+
sources: external_exports.array(external_exports.string()),
|
|
23936
|
+
evidence: external_exports.string(),
|
|
23937
|
+
plain: explanationSchema,
|
|
23938
|
+
technical: explanationSchema,
|
|
23939
|
+
prompt: external_exports.string(),
|
|
23940
|
+
validation: external_exports.object({
|
|
23941
|
+
status: external_exports.enum([
|
|
23942
|
+
"repeatable",
|
|
23943
|
+
"not-reproduced",
|
|
23944
|
+
"inconclusive",
|
|
23945
|
+
"not-tested"
|
|
23946
|
+
]),
|
|
23947
|
+
detail: external_exports.string()
|
|
23948
|
+
}).optional(),
|
|
23949
|
+
triage: findingTriageSchema.optional()
|
|
23950
|
+
});
|
|
23951
|
+
var PULSE_BOOKING_SERVICES = [
|
|
23952
|
+
"calendly",
|
|
23953
|
+
"cal-com",
|
|
23954
|
+
"savvycal",
|
|
23955
|
+
"acuity",
|
|
23956
|
+
"hubspot-meetings",
|
|
23957
|
+
"zoom",
|
|
23958
|
+
"stripe-payment-link"
|
|
23959
|
+
];
|
|
23960
|
+
var PULSE_COMMUNITY_SERVICES = ["skool"];
|
|
23961
|
+
var PULSE_VISITOR_DATA_SERVICES = [
|
|
23962
|
+
"amplitude",
|
|
23963
|
+
"google-analytics",
|
|
23964
|
+
"hotjar",
|
|
23965
|
+
"mixpanel",
|
|
23966
|
+
"posthog",
|
|
23967
|
+
"segment",
|
|
23968
|
+
"sentry"
|
|
23969
|
+
];
|
|
23970
|
+
var PULSE_DNS_HOSTS = [
|
|
23971
|
+
"wix",
|
|
23972
|
+
"cloudflare",
|
|
23973
|
+
"godaddy",
|
|
23974
|
+
"namecheap",
|
|
23975
|
+
"route53",
|
|
23976
|
+
"squarespace",
|
|
23977
|
+
"google-cloud-dns",
|
|
23978
|
+
"vercel",
|
|
23979
|
+
"netlify",
|
|
23980
|
+
"hostinger",
|
|
23981
|
+
"ionos",
|
|
23982
|
+
"porkbun",
|
|
23983
|
+
"dnsimple"
|
|
23984
|
+
];
|
|
23985
|
+
var PULSE_MAIL_PROVIDERS = [
|
|
23986
|
+
"google-workspace",
|
|
23987
|
+
"microsoft-365",
|
|
23988
|
+
"zoho",
|
|
23989
|
+
"proton",
|
|
23990
|
+
"fastmail"
|
|
23991
|
+
];
|
|
23992
|
+
var PULSE_EMAIL_SENDERS = [
|
|
23993
|
+
"google-workspace",
|
|
23994
|
+
"microsoft-365",
|
|
23995
|
+
"sendgrid",
|
|
23996
|
+
"mailgun",
|
|
23997
|
+
"amazon-ses",
|
|
23998
|
+
"postmark",
|
|
23999
|
+
"mailchimp",
|
|
24000
|
+
"hubspot",
|
|
24001
|
+
"salesforce",
|
|
24002
|
+
"zendesk",
|
|
24003
|
+
"resend"
|
|
24004
|
+
];
|
|
24005
|
+
var PULSE_CHAIN_IDS = ["impersonation", "borrowed-address"];
|
|
24006
|
+
var day = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
|
24007
|
+
var attackChainSchema = external_exports.object({
|
|
24008
|
+
id: external_exports.enum(PULSE_CHAIN_IDS),
|
|
24009
|
+
severity: external_exports.literal("high"),
|
|
24010
|
+
confidence: external_exports.enum(["observed", "potential"]),
|
|
24011
|
+
/** Evidence ids: finding ids, or `contact-surface:*`, `email-policy:*`, `configuration:*`. */
|
|
24012
|
+
ingredients: external_exports.array(external_exports.string()),
|
|
24013
|
+
strengtheners: external_exports.array(external_exports.string())
|
|
24014
|
+
});
|
|
24015
|
+
var certificateRetrievalSchema = external_exports.object({
|
|
24016
|
+
mode: external_exports.enum(["live", "shared-cache", "memory-cache"]),
|
|
24017
|
+
retrievedAt: external_exports.iso.datetime().optional(),
|
|
24018
|
+
expiresAt: external_exports.iso.datetime().optional()
|
|
24019
|
+
}).strict();
|
|
24020
|
+
var domainMapSchema = external_exports.object({
|
|
24021
|
+
domain: external_exports.string(),
|
|
24022
|
+
/** `crtsh-sql` is crt.sh's database; `certspotter` covers current certificates only. */
|
|
24023
|
+
source: external_exports.enum(["crt.sh", "crtsh-sql", "certspotter"]),
|
|
24024
|
+
retrieval: certificateRetrievalSchema.optional(),
|
|
24025
|
+
names: external_exports.array(
|
|
24026
|
+
external_exports.object({
|
|
24027
|
+
name: external_exports.string(),
|
|
24028
|
+
controller: external_exports.enum(["you", "third-party", "nobody"]).optional(),
|
|
24029
|
+
chain: external_exports.array(external_exports.string()),
|
|
24030
|
+
firstSeen: day,
|
|
24031
|
+
lastIssued: day,
|
|
24032
|
+
lastExpiry: day
|
|
24033
|
+
})
|
|
24034
|
+
)
|
|
24035
|
+
});
|
|
24036
|
+
var targetRegistrationSchema = external_exports.discriminatedUnion("state", [
|
|
24037
|
+
external_exports.object({
|
|
24038
|
+
state: external_exports.literal("available"),
|
|
24039
|
+
domain: external_exports.string().min(1).max(253),
|
|
24040
|
+
expiresAt: external_exports.iso.date().optional(),
|
|
24041
|
+
statuses: external_exports.array(external_exports.string().max(128)).max(128)
|
|
24042
|
+
}).strict(),
|
|
24043
|
+
external_exports.object({
|
|
24044
|
+
state: external_exports.enum([
|
|
24045
|
+
"not-found",
|
|
24046
|
+
"unsupported",
|
|
24047
|
+
"unavailable",
|
|
24048
|
+
"not-configured"
|
|
24049
|
+
]),
|
|
24050
|
+
domain: external_exports.string().min(1).max(253)
|
|
24051
|
+
}).strict(),
|
|
24052
|
+
external_exports.object({ state: external_exports.literal("not-applicable") }).strict()
|
|
24053
|
+
]);
|
|
24054
|
+
var publicRecordsSchema = external_exports.object({
|
|
24055
|
+
readAt: external_exports.string(),
|
|
24056
|
+
certificateHistory: external_exports.enum(["available", "unavailable", "not-configured"]),
|
|
24057
|
+
policyQueries: external_exports.number().int().nonnegative(),
|
|
24058
|
+
sweepQueries: external_exports.number().int().nonnegative(),
|
|
24059
|
+
spfQueries: external_exports.number().int().nonnegative(),
|
|
24060
|
+
rdapLookups: external_exports.number().int().nonnegative(),
|
|
24061
|
+
targetRegistrationLookups: external_exports.number().int().min(0).max(1).optional(),
|
|
24062
|
+
targetRegistration: targetRegistrationSchema.optional(),
|
|
24063
|
+
/** DS presence queries over DNS-over-HTTPS; absent when not configured. */
|
|
24064
|
+
dnssecQueries: external_exports.number().int().nonnegative().optional()
|
|
24065
|
+
});
|
|
24066
|
+
var contactSurfaceSchema = external_exports.object({
|
|
24067
|
+
emails: external_exports.number().int().nonnegative(),
|
|
24068
|
+
onDomainEmails: external_exports.array(external_exports.string()),
|
|
24069
|
+
phones: external_exports.number().int().nonnegative(),
|
|
24070
|
+
booking: external_exports.array(external_exports.enum(PULSE_BOOKING_SERVICES)),
|
|
24071
|
+
community: external_exports.array(external_exports.enum(PULSE_COMMUNITY_SERVICES))
|
|
24072
|
+
});
|
|
24073
|
+
var providersSchema = external_exports.object({
|
|
24074
|
+
dns: external_exports.enum(PULSE_DNS_HOSTS).optional(),
|
|
24075
|
+
mail: external_exports.enum(PULSE_MAIL_PROVIDERS).optional(),
|
|
24076
|
+
senders: external_exports.array(external_exports.enum(PULSE_EMAIL_SENDERS))
|
|
24077
|
+
});
|
|
24078
|
+
var apiAccessClassificationSchema = external_exports.enum([
|
|
24079
|
+
"protected",
|
|
24080
|
+
"open-data",
|
|
24081
|
+
"open-sensitive",
|
|
24082
|
+
"open-metadata",
|
|
24083
|
+
"error-disclosure",
|
|
24084
|
+
"auth-mismatch",
|
|
24085
|
+
"absent"
|
|
24086
|
+
]);
|
|
24087
|
+
var baasValueClassificationSchema = external_exports.enum([
|
|
24088
|
+
"protected",
|
|
24089
|
+
"open-data",
|
|
24090
|
+
"open-sensitive",
|
|
24091
|
+
"open-metadata",
|
|
24092
|
+
"error-disclosure",
|
|
24093
|
+
"auth-mismatch",
|
|
24094
|
+
"absent",
|
|
24095
|
+
"inconclusive"
|
|
24096
|
+
]);
|
|
24097
|
+
var apiSurfaceSchema = external_exports.object({
|
|
24098
|
+
counts: external_exports.object({
|
|
24099
|
+
mapped: external_exports.number().int().nonnegative(),
|
|
24100
|
+
protected: external_exports.number().int().nonnegative(),
|
|
24101
|
+
open: external_exports.number().int().nonnegative(),
|
|
24102
|
+
notChecked: external_exports.number().int().nonnegative(),
|
|
24103
|
+
absent: external_exports.number().int().nonnegative()
|
|
24104
|
+
}),
|
|
24105
|
+
/** The endpoints that answered with data to an unauthenticated GET. */
|
|
24106
|
+
open: external_exports.array(
|
|
24107
|
+
external_exports.object({
|
|
24108
|
+
method: external_exports.enum(["GET", "HEAD"]),
|
|
24109
|
+
path: external_exports.string(),
|
|
24110
|
+
status: external_exports.number().int().min(0).max(599),
|
|
24111
|
+
contentType: external_exports.string(),
|
|
24112
|
+
classification: apiAccessClassificationSchema,
|
|
24113
|
+
/** Top-level field names only, never their values. */
|
|
24114
|
+
fields: external_exports.array(external_exports.string()).max(20).optional(),
|
|
24115
|
+
records: external_exports.number().int().nonnegative().optional()
|
|
24116
|
+
})
|
|
24117
|
+
).max(50),
|
|
24118
|
+
graphql: external_exports.object({
|
|
24119
|
+
endpoint: external_exports.string(),
|
|
24120
|
+
introspection: external_exports.enum(["enabled", "not-checked"]),
|
|
24121
|
+
queries: external_exports.number().int().nonnegative().optional(),
|
|
24122
|
+
mutations: external_exports.number().int().nonnegative().optional()
|
|
24123
|
+
}).optional(),
|
|
24124
|
+
/**
|
|
24125
|
+
* Anonymous reads of the NextAuth/Auth.js and Better Auth session routes
|
|
24126
|
+
* (engine 2.13). Absent routes are dropped; field names only, never values.
|
|
24127
|
+
*/
|
|
24128
|
+
authSessions: external_exports.array(
|
|
24129
|
+
external_exports.object({
|
|
24130
|
+
path: external_exports.string(),
|
|
24131
|
+
library: external_exports.enum(["authjs", "better-auth"]),
|
|
24132
|
+
status: external_exports.number().int().min(0).max(599),
|
|
24133
|
+
classification: external_exports.enum(["protected", "anonymous", "session-exposed"]),
|
|
24134
|
+
sharedCacheable: external_exports.boolean().optional(),
|
|
24135
|
+
fields: external_exports.array(external_exports.string()).max(20).optional()
|
|
24136
|
+
})
|
|
24137
|
+
).max(2).optional(),
|
|
24138
|
+
/**
|
|
24139
|
+
* Managed-backend follow-through (engine 2.14): Supabase REST tables and
|
|
24140
|
+
* Firebase realtime databases the anon key or a public read could answer.
|
|
24141
|
+
* Table and key names only, never values.
|
|
24142
|
+
*/
|
|
24143
|
+
baas: external_exports.object({
|
|
24144
|
+
supabase: external_exports.array(
|
|
24145
|
+
external_exports.object({
|
|
24146
|
+
url: external_exports.string(),
|
|
24147
|
+
tables: external_exports.array(
|
|
24148
|
+
external_exports.object({
|
|
24149
|
+
table: external_exports.string(),
|
|
24150
|
+
status: external_exports.number().int().min(0).max(599),
|
|
24151
|
+
classification: baasValueClassificationSchema,
|
|
24152
|
+
/** Declared column names only, never their values. */
|
|
24153
|
+
fields: external_exports.array(external_exports.string()).max(20).optional(),
|
|
24154
|
+
/** Top-level field count when no declared name is published. */
|
|
24155
|
+
fieldCount: external_exports.number().int().nonnegative().optional(),
|
|
24156
|
+
records: external_exports.number().int().nonnegative().optional()
|
|
24157
|
+
})
|
|
24158
|
+
).max(5),
|
|
24159
|
+
unprobed: external_exports.number().int().nonnegative().optional()
|
|
24160
|
+
})
|
|
24161
|
+
).max(1).optional(),
|
|
24162
|
+
firebase: external_exports.array(
|
|
24163
|
+
external_exports.object({
|
|
24164
|
+
url: external_exports.string(),
|
|
24165
|
+
status: external_exports.number().int().min(0).max(599),
|
|
24166
|
+
classification: external_exports.enum(["public", "protected", "absent"]),
|
|
24167
|
+
/** Top-level key count only; a key can itself be customer data. */
|
|
24168
|
+
keyCount: external_exports.number().int().nonnegative().optional()
|
|
24169
|
+
})
|
|
24170
|
+
).max(1).optional(),
|
|
24171
|
+
convex: external_exports.array(
|
|
24172
|
+
external_exports.object({
|
|
24173
|
+
url: external_exports.string(),
|
|
24174
|
+
functions: external_exports.array(
|
|
24175
|
+
external_exports.object({
|
|
24176
|
+
path: external_exports.string(),
|
|
24177
|
+
status: external_exports.number().int().min(0).max(599),
|
|
24178
|
+
classification: apiAccessClassificationSchema,
|
|
24179
|
+
/** Field count only; a value can be keyed by customer data. */
|
|
24180
|
+
fieldCount: external_exports.number().int().nonnegative().optional(),
|
|
24181
|
+
records: external_exports.number().int().nonnegative().optional()
|
|
24182
|
+
})
|
|
24183
|
+
).max(5),
|
|
24184
|
+
unprobed: external_exports.number().int().nonnegative().optional()
|
|
24185
|
+
})
|
|
24186
|
+
).max(1).optional()
|
|
24187
|
+
}).optional(),
|
|
24188
|
+
/**
|
|
24189
|
+
* Deep's coverage ledger (C2): every investigation packet closed as found,
|
|
24190
|
+
* not found, not applicable or could not check, with a reason. This is the
|
|
24191
|
+
* customer-visible "what we checked", made systematic. It carries no prompt,
|
|
24192
|
+
* tool choice or ordering detail, only the packet, its impact, the outcome,
|
|
24193
|
+
* a coded reason and the counts.
|
|
24194
|
+
*/
|
|
24195
|
+
coverageLedger: external_exports.object({
|
|
24196
|
+
version: external_exports.literal("1"),
|
|
24197
|
+
entries: external_exports.array(
|
|
24198
|
+
external_exports.object({
|
|
24199
|
+
packet: external_exports.enum([
|
|
24200
|
+
"supabase-rls",
|
|
24201
|
+
"storage-buckets",
|
|
24202
|
+
"script-routes",
|
|
24203
|
+
"auth-endpoints"
|
|
24204
|
+
]),
|
|
24205
|
+
impact: external_exports.enum([
|
|
24206
|
+
"data-exposure",
|
|
24207
|
+
"auth-surface",
|
|
24208
|
+
"exposed-files",
|
|
24209
|
+
"configuration"
|
|
24210
|
+
]),
|
|
24211
|
+
outcome: external_exports.enum([
|
|
24212
|
+
"found",
|
|
24213
|
+
"not-found",
|
|
24214
|
+
"not-applicable",
|
|
24215
|
+
"could-not-check"
|
|
24216
|
+
]),
|
|
24217
|
+
reason: external_exports.string(),
|
|
24218
|
+
requests: external_exports.number().int().nonnegative(),
|
|
24219
|
+
budget: external_exports.number().int().nonnegative(),
|
|
24220
|
+
findings: external_exports.number().int().nonnegative().optional(),
|
|
24221
|
+
references: external_exports.number().int().nonnegative().optional()
|
|
24222
|
+
})
|
|
24223
|
+
).max(20)
|
|
24224
|
+
}).optional(),
|
|
24225
|
+
truncated: external_exports.number().int().nonnegative().optional()
|
|
24226
|
+
});
|
|
24227
|
+
var SOURCE_MAP_BASELINES = [
|
|
24228
|
+
"published",
|
|
24229
|
+
"none",
|
|
24230
|
+
"not-checked"
|
|
24231
|
+
];
|
|
24232
|
+
var sourceMapBaselineSchema = external_exports.enum(SOURCE_MAP_BASELINES);
|
|
24233
|
+
var EXPOSURE_CHECK_STATES = [
|
|
24234
|
+
"clear",
|
|
24235
|
+
"found",
|
|
24236
|
+
"not-run",
|
|
24237
|
+
"could-not-run"
|
|
24238
|
+
];
|
|
24239
|
+
var EXPOSURE_CHECK_LIMITS = [
|
|
24240
|
+
"no-source-map",
|
|
24241
|
+
"source-map-unread",
|
|
24242
|
+
"budget"
|
|
24243
|
+
];
|
|
24244
|
+
var exposureCheckSchema = external_exports.object({
|
|
24245
|
+
state: external_exports.enum(EXPOSURE_CHECK_STATES),
|
|
24246
|
+
/** Empty when discovery ran at full confidence. */
|
|
24247
|
+
limits: external_exports.array(external_exports.enum(EXPOSURE_CHECK_LIMITS)).max(3),
|
|
24248
|
+
sourceMap: sourceMapBaselineSchema
|
|
24249
|
+
});
|
|
24250
|
+
function exposureCheckHeadline(check3) {
|
|
24251
|
+
return check3.state === "clear" && check3.limits.length ? "clear-limited" : check3.state;
|
|
24252
|
+
}
|
|
24253
|
+
var EXPOSURE_CHECK_TEXT = {
|
|
24254
|
+
title: {
|
|
24255
|
+
clear: "No unintended public API routes or pages found exposed.",
|
|
24256
|
+
"clear-limited": "No exposed API routes or pages found, but coverage was limited, so this is not a full all-clear.",
|
|
24257
|
+
found: "Deep found API routes or pages that answer without sign-in.",
|
|
24258
|
+
"not-run": "This run did not check for exposed API routes and pages.",
|
|
24259
|
+
"could-not-run": "Deep could not finish checking for exposed API routes and pages, so this report makes no claim about them."
|
|
24260
|
+
},
|
|
24261
|
+
limits: {
|
|
24262
|
+
"no-source-map": "Your site publishes no source maps, so Deep had fewer routes and pages to find.",
|
|
24263
|
+
"source-map-unread": "Deep did not read your source maps on this run, so it had fewer routes and pages to find.",
|
|
24264
|
+
budget: "Deep reached its request limit before it finished looking."
|
|
24265
|
+
}
|
|
24266
|
+
};
|
|
24267
|
+
function exposureCheckLines(check3) {
|
|
24268
|
+
return [
|
|
24269
|
+
EXPOSURE_CHECK_TEXT.title[exposureCheckHeadline(check3)],
|
|
24270
|
+
...check3.state === "clear" ? check3.limits.map((limit) => EXPOSURE_CHECK_TEXT.limits[limit]) : []
|
|
24271
|
+
];
|
|
24272
|
+
}
|
|
24273
|
+
var publicPulseReportSchema = external_exports.object({
|
|
24274
|
+
schemaVersion: external_exports.literal("public-pulse@1"),
|
|
24275
|
+
assessment: assessmentEvidenceSchema.optional(),
|
|
24276
|
+
standards: standardsSummarySchema.optional(),
|
|
24277
|
+
target: external_exports.object({
|
|
24278
|
+
shape: external_exports.enum(["html", "client-rendered", "api", "document"]),
|
|
24279
|
+
signals: external_exports.array(
|
|
24280
|
+
external_exports.enum([
|
|
24281
|
+
"structured-json",
|
|
24282
|
+
"html-response",
|
|
24283
|
+
"small-html",
|
|
24284
|
+
"empty-root",
|
|
24285
|
+
"script-reference",
|
|
24286
|
+
"few-links"
|
|
24287
|
+
])
|
|
24288
|
+
).max(6).optional(),
|
|
24289
|
+
rendering: external_exports.enum([
|
|
24290
|
+
"not-requested",
|
|
24291
|
+
"complete",
|
|
24292
|
+
"limited",
|
|
24293
|
+
"unavailable"
|
|
24294
|
+
]),
|
|
24295
|
+
blockedRequests: external_exports.number().int().nonnegative(),
|
|
24296
|
+
observedCookies: external_exports.number().int().nonnegative().optional(),
|
|
24297
|
+
secureCookies: external_exports.number().int().nonnegative().optional(),
|
|
24298
|
+
httpOnlyCookies: external_exports.number().int().nonnegative().optional(),
|
|
24299
|
+
/** Built-with chips, shared stack vocabulary. Not a finding. */
|
|
24300
|
+
stack: external_exports.array(quickStackSchema).max(PULSE_STACK_MAX).optional(),
|
|
24301
|
+
/** Explicit technology and version declarations; never findings. */
|
|
24302
|
+
technologies: external_exports.object({
|
|
24303
|
+
limited: external_exports.boolean(),
|
|
24304
|
+
entries: external_exports.array(
|
|
24305
|
+
external_exports.object({
|
|
24306
|
+
id: quickStackSchema,
|
|
24307
|
+
version: external_exports.string().regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}(?:-[A-Za-z0-9.-]{1,40})?$/).optional(),
|
|
24308
|
+
via: external_exports.enum(["header", "generator", "asset", "code", "banner"])
|
|
24309
|
+
})
|
|
24310
|
+
).max(24)
|
|
24311
|
+
}).optional(),
|
|
24312
|
+
/**
|
|
24313
|
+
* Companies whose code runs on the page: external and preloaded
|
|
24314
|
+
* scripts by registrable domain. Not a finding.
|
|
24315
|
+
*/
|
|
24316
|
+
thirdPartyCode: external_exports.array(
|
|
24317
|
+
external_exports.object({
|
|
24318
|
+
domain: external_exports.string(),
|
|
24319
|
+
stack: quickStackSchema.optional(),
|
|
24320
|
+
integrity: external_exports.enum(["declared", "missing"])
|
|
24321
|
+
})
|
|
24322
|
+
).optional(),
|
|
24323
|
+
/**
|
|
24324
|
+
* Companies the page sends visitor activity to, from known endpoints
|
|
24325
|
+
* in its own script text. Never companies that run code. Not a finding.
|
|
24326
|
+
*/
|
|
24327
|
+
thirdPartyData: external_exports.array(
|
|
24328
|
+
external_exports.object({
|
|
24329
|
+
service: external_exports.enum(PULSE_VISITOR_DATA_SERVICES),
|
|
24330
|
+
hosts: external_exports.array(external_exports.string())
|
|
24331
|
+
})
|
|
24332
|
+
).optional(),
|
|
24333
|
+
/** Owner only. */
|
|
24334
|
+
contactSurface: contactSurfaceSchema.optional(),
|
|
24335
|
+
/** Owner only. */
|
|
24336
|
+
providers: providersSchema.optional()
|
|
24337
|
+
}).optional(),
|
|
24338
|
+
methodology: external_exports.literal("public-surface"),
|
|
24339
|
+
methodologyVersion: external_exports.literal("1"),
|
|
24340
|
+
checks: external_exports.array(publicCheckSchema),
|
|
24341
|
+
surface: external_exports.object({
|
|
24342
|
+
findings: external_exports.array(publicFindingSchema),
|
|
24343
|
+
chains: external_exports.array(attackChainSchema).optional(),
|
|
24344
|
+
/** Deep only. Counts and the endpoints that answered without sign-in. */
|
|
24345
|
+
apiSurface: apiSurfaceSchema.optional(),
|
|
24346
|
+
/** Owner only. */
|
|
24347
|
+
domainMap: domainMapSchema.optional(),
|
|
24348
|
+
/** Owner only. When public records were read and what that cost. */
|
|
24349
|
+
publicRecords: publicRecordsSchema.optional()
|
|
24350
|
+
}).optional(),
|
|
24351
|
+
score: external_exports.number().min(0).max(100).nullable(),
|
|
24352
|
+
coverage: external_exports.number().min(0).max(100),
|
|
24353
|
+
pages: external_exports.number().int().nonnegative(),
|
|
24354
|
+
requests: external_exports.number().int().nonnegative(),
|
|
24355
|
+
limitations: external_exports.array(external_exports.string()),
|
|
24356
|
+
stoppedReason: external_exports.string().optional(),
|
|
24357
|
+
/**
|
|
24358
|
+
* Deep only, and present even when the run stopped before its surface
|
|
24359
|
+
* stage: whether unintended public API routes and pages were found.
|
|
24360
|
+
*/
|
|
24361
|
+
exposureCheck: exposureCheckSchema.optional(),
|
|
24362
|
+
/** How far collection got (engine 2.18+; derived for older reports). */
|
|
24363
|
+
outcome: publicRunOutcomeSchema.optional()
|
|
24364
|
+
});
|
|
24365
|
+
var publicPulseChangeSchema = external_exports.object({
|
|
24366
|
+
id: external_exports.string(),
|
|
24367
|
+
kind: external_exports.enum(["new", "persistent", "absent", "unknown"]),
|
|
24368
|
+
area: external_exports.enum(["configuration", "exposure"]),
|
|
24369
|
+
title: external_exports.string(),
|
|
24370
|
+
severity: external_exports.string(),
|
|
24371
|
+
explanation: external_exports.string(),
|
|
24372
|
+
/** What the recheck proves about an earlier finding; absent for a new one. */
|
|
24373
|
+
outcome: recheckStatusSchema.optional()
|
|
24374
|
+
});
|
|
24375
|
+
|
|
23777
24376
|
// ../../packages/contracts/src/cli-routes.ts
|
|
23778
24377
|
var CLI_API = "/api/cli/v1";
|
|
23779
24378
|
var segment = (value) => encodeURIComponent(value);
|
|
@@ -23786,6 +24385,8 @@ var CLI_API_ROUTES = {
|
|
|
23786
24385
|
projectStatus: (projectId) => `${CLI_API}/projects/${segment(projectId)}/status`,
|
|
23787
24386
|
/** POST: start a Pulse (needs the `pulse` scope). */
|
|
23788
24387
|
projectPulse: (projectId) => `${CLI_API}/projects/${segment(projectId)}/pulse`,
|
|
24388
|
+
/** POST: start a release's Deep scan (a CI key with the `deep` scope). */
|
|
24389
|
+
projectDeep: (projectId) => `${CLI_API}/projects/${segment(projectId)}/deep`,
|
|
23789
24390
|
runs: `${CLI_API}/runs`,
|
|
23790
24391
|
run: (runId) => `${CLI_API}/runs/${segment(runId)}`,
|
|
23791
24392
|
findings: (runId) => `${CLI_API}/runs/${segment(runId)}/findings`,
|
|
@@ -23978,7 +24579,18 @@ var cliRunDetailSchema = cliRunSummarySchema.extend({
|
|
|
23978
24579
|
percent: external_exports.number().nullable(),
|
|
23979
24580
|
limitations: external_exports.array(external_exports.string())
|
|
23980
24581
|
}),
|
|
23981
|
-
caveats: external_exports.array(external_exports.string())
|
|
24582
|
+
caveats: external_exports.array(external_exports.string()),
|
|
24583
|
+
/**
|
|
24584
|
+
* Pulse, Standard and Deep: how far collection got (blocked, unreachable
|
|
24585
|
+
* and so on) and what the report is based on. Null for pentests and
|
|
24586
|
+
* unfinished runs.
|
|
24587
|
+
*/
|
|
24588
|
+
outcome: publicRunOutcomeSchema.nullable().optional(),
|
|
24589
|
+
/**
|
|
24590
|
+
* Deep only: whether unintended public API routes and pages were found,
|
|
24591
|
+
* as a state and coded limits. Never the routes requested. Null otherwise.
|
|
24592
|
+
*/
|
|
24593
|
+
exposureCheck: exposureCheckSchema.nullable().optional()
|
|
23982
24594
|
});
|
|
23983
24595
|
var CLI_FINDING_STATES = [
|
|
23984
24596
|
"attention",
|
|
@@ -24212,7 +24824,18 @@ var agentScanSchema = external_exports.strictObject({
|
|
|
24212
24824
|
percent: external_exports.number().nullable(),
|
|
24213
24825
|
limitations: external_exports.array(external_exports.string())
|
|
24214
24826
|
}),
|
|
24215
|
-
caveats: external_exports.array(external_exports.string())
|
|
24827
|
+
caveats: external_exports.array(external_exports.string()),
|
|
24828
|
+
/**
|
|
24829
|
+
* How far collection got and what the report is based on. Only a `full`
|
|
24830
|
+
* run is ever graded; the others say why (a bot check, no DNS address).
|
|
24831
|
+
* Null for pentests and unfinished runs.
|
|
24832
|
+
*/
|
|
24833
|
+
outcome: publicRunOutcomeSchema.nullable(),
|
|
24834
|
+
/**
|
|
24835
|
+
* Deep only: whether unintended public API routes and pages were found.
|
|
24836
|
+
* Absent for Pulse, Standard and pentests, and from older servers.
|
|
24837
|
+
*/
|
|
24838
|
+
exposureCheck: exposureCheckSchema.optional()
|
|
24216
24839
|
});
|
|
24217
24840
|
var agentSiteSchema = external_exports.strictObject({
|
|
24218
24841
|
id: agentIdSchema,
|
|
@@ -24909,14 +25532,53 @@ async function readLockfiles(dir) {
|
|
|
24909
25532
|
return { packages, sources, skipped };
|
|
24910
25533
|
}
|
|
24911
25534
|
|
|
25535
|
+
// src/terminal.ts
|
|
25536
|
+
var UNSAFE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f--]/g;
|
|
25537
|
+
function neutralize(text2) {
|
|
25538
|
+
return text2.replace(/\r\n/g, "\n").replace(UNSAFE, (char) => {
|
|
25539
|
+
const code = char.charCodeAt(0);
|
|
25540
|
+
if (code < 32) return String.fromCharCode(9216 + code);
|
|
25541
|
+
if (code === 127) return "\u2421";
|
|
25542
|
+
return "\uFFFD";
|
|
25543
|
+
});
|
|
25544
|
+
}
|
|
25545
|
+
function neutralizeDeep(value) {
|
|
25546
|
+
if (typeof value === "string") return neutralize(value);
|
|
25547
|
+
if (Array.isArray(value)) return value.map(neutralizeDeep);
|
|
25548
|
+
if (value && typeof value === "object") {
|
|
25549
|
+
const copy = {};
|
|
25550
|
+
for (const [key, child] of Object.entries(value))
|
|
25551
|
+
copy[key] = neutralizeDeep(child);
|
|
25552
|
+
return copy;
|
|
25553
|
+
}
|
|
25554
|
+
return value;
|
|
25555
|
+
}
|
|
25556
|
+
var OWN_STYLE = /\u001b\[(?:1|2|22|31|32|33|36|39)m/g;
|
|
25557
|
+
function terminalSafe(text2) {
|
|
25558
|
+
const styles = text2.match(OWN_STYLE) ?? [];
|
|
25559
|
+
return text2.split(OWN_STYLE).map((part, index) => neutralize(part) + (styles[index] ?? "")).join("");
|
|
25560
|
+
}
|
|
25561
|
+
function terminalWidth(stream) {
|
|
25562
|
+
return stream.isTTY && stream.columns ? stream.columns : null;
|
|
25563
|
+
}
|
|
25564
|
+
|
|
24912
25565
|
// src/api.ts
|
|
24913
25566
|
function friendlyError(status2, message, replace) {
|
|
24914
25567
|
if (status2 === 404 && message === "Not found" && replace.notFound)
|
|
24915
25568
|
return replace.notFound;
|
|
25569
|
+
const permission = status2 === 403 && message?.match(/^Missing required permission: (\S+)$/);
|
|
25570
|
+
if (permission)
|
|
25571
|
+
return PERMISSION_TEXT[permission[1]] ?? `Your role does not allow this (it needs ${permission[1]}).`;
|
|
24916
25572
|
if (status2 === 403 && message === "This CLI key does not have the required scope" && replace.forbidden)
|
|
24917
25573
|
return replace.forbidden;
|
|
24918
25574
|
return message ?? null;
|
|
24919
25575
|
}
|
|
25576
|
+
var PERMISSION_TEXT = {
|
|
25577
|
+
"audit.launch": "Your role on this site lets you read results, not start scans. Ask a project manager for the contributor role.",
|
|
25578
|
+
"project.read": "You do not have access to this site.",
|
|
25579
|
+
"report.read": "You do not have access to this site's results.",
|
|
25580
|
+
"project.manage": "Only a project manager or workspace admin can change this site."
|
|
25581
|
+
};
|
|
24920
25582
|
var missingRun = (runId) => `No run "${runId}" in this workspace.`;
|
|
24921
25583
|
var LockpickApiError = class extends Error {
|
|
24922
25584
|
constructor(message, status2, code, agentError = null) {
|
|
@@ -24991,10 +25653,10 @@ function mappedLocalIPv4(name) {
|
|
|
24991
25653
|
return localIPv4([high >> 8, high & 255, low >> 8, low & 255]);
|
|
24992
25654
|
}
|
|
24993
25655
|
function normalizeHost(value) {
|
|
24994
|
-
const
|
|
25656
|
+
const explicit = /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
|
|
24995
25657
|
let url3;
|
|
24996
25658
|
try {
|
|
24997
|
-
url3 = new URL(
|
|
25659
|
+
url3 = new URL(explicit ? value : `http://${value}`);
|
|
24998
25660
|
} catch {
|
|
24999
25661
|
throw new LockpickApiError(
|
|
25000
25662
|
"Invalid Lockpick host URL.",
|
|
@@ -25002,9 +25664,8 @@ function normalizeHost(value) {
|
|
|
25002
25664
|
"VALIDATION_ERROR"
|
|
25003
25665
|
);
|
|
25004
25666
|
}
|
|
25005
|
-
|
|
25006
|
-
const
|
|
25007
|
-
const local = name === "localhost" || name === "::1" || ipKind === 6 && (/^f[cd]/.test(name) || mappedLocalIPv4(name)) || ipKind === 4 && localIPv4(name.split(".").map(Number)) || ipKind === 0 && (!name.includes(".") || name.endsWith(".local") || name.endsWith(".ts.net"));
|
|
25667
|
+
if (!explicit && !localHostname(url3.hostname)) url3.protocol = "https:";
|
|
25668
|
+
const local = localHostname(url3.hostname);
|
|
25008
25669
|
if (url3.protocol !== "https:" && !(url3.protocol === "http:" && local) || url3.username || url3.password || url3.search || url3.hash)
|
|
25009
25670
|
throw new LockpickApiError(
|
|
25010
25671
|
"Use an HTTPS Lockpick host, or HTTP on a local network; omit credentials, query and fragment.",
|
|
@@ -25013,6 +25674,37 @@ function normalizeHost(value) {
|
|
|
25013
25674
|
);
|
|
25014
25675
|
return url3.origin;
|
|
25015
25676
|
}
|
|
25677
|
+
function localHostname(hostname4) {
|
|
25678
|
+
const name = hostname4.toLowerCase().replace(/^\[|\]$/g, "");
|
|
25679
|
+
const ipKind = isIP(name);
|
|
25680
|
+
return name === "localhost" || name === "::1" || ipKind === 6 && (/^f[cd]/.test(name) || mappedLocalIPv4(name)) || ipKind === 4 && localIPv4(name.split(".").map(Number)) || ipKind === 0 && (!name.includes(".") || name.endsWith(".local") || name.endsWith(".ts.net"));
|
|
25681
|
+
}
|
|
25682
|
+
var HostRedirectError = class extends LockpickApiError {
|
|
25683
|
+
constructor(host, canonicalHost, status2) {
|
|
25684
|
+
super(
|
|
25685
|
+
canonicalHost ? `${host} redirects to ${canonicalHost}. Use \`--host ${canonicalHost}\`.` : `${host} redirected the request elsewhere. Check the Lockpick host.`,
|
|
25686
|
+
status2,
|
|
25687
|
+
"HOST_REDIRECT"
|
|
25688
|
+
);
|
|
25689
|
+
this.host = host;
|
|
25690
|
+
this.canonicalHost = canonicalHost;
|
|
25691
|
+
}
|
|
25692
|
+
host;
|
|
25693
|
+
canonicalHost;
|
|
25694
|
+
};
|
|
25695
|
+
function hostRedirect(host, requested, response) {
|
|
25696
|
+
if (response.status < 300 || response.status > 399) return null;
|
|
25697
|
+
let canonical = null;
|
|
25698
|
+
try {
|
|
25699
|
+
const target = new URL(response.headers.get("location") ?? "", requested);
|
|
25700
|
+
const origin = normalizeHost(target.origin);
|
|
25701
|
+
if (origin !== host && target.pathname === requested.pathname)
|
|
25702
|
+
canonical = origin;
|
|
25703
|
+
} catch {
|
|
25704
|
+
canonical = null;
|
|
25705
|
+
}
|
|
25706
|
+
return new HostRedirectError(host, canonical, response.status);
|
|
25707
|
+
}
|
|
25016
25708
|
var pageQuery = (query) => Object.fromEntries(
|
|
25017
25709
|
Object.entries(query).filter(([, value]) => value !== void 0 && value !== false).map(([name, value]) => [name, String(value)])
|
|
25018
25710
|
);
|
|
@@ -25025,8 +25717,14 @@ var LockpickClient = class {
|
|
|
25025
25717
|
host;
|
|
25026
25718
|
key;
|
|
25027
25719
|
fetchImpl;
|
|
25720
|
+
/**
|
|
25721
|
+
* Server text as the CLI may show it: the key never appears, and terminal
|
|
25722
|
+
* control characters are neutralized (`terminal.ts`).
|
|
25723
|
+
*/
|
|
25028
25724
|
redact(text2) {
|
|
25029
|
-
return
|
|
25725
|
+
return neutralize(
|
|
25726
|
+
this.key ? text2.replaceAll(this.key, "[redacted]") : text2
|
|
25727
|
+
);
|
|
25030
25728
|
}
|
|
25031
25729
|
redactAgent(error51) {
|
|
25032
25730
|
return {
|
|
@@ -25095,7 +25793,8 @@ var LockpickClient = class {
|
|
|
25095
25793
|
method: init.method ?? "GET",
|
|
25096
25794
|
headers,
|
|
25097
25795
|
body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
|
|
25098
|
-
|
|
25796
|
+
// Never followed (the key must not travel); see `hostRedirect`.
|
|
25797
|
+
redirect: "manual",
|
|
25099
25798
|
signal: controller.signal
|
|
25100
25799
|
}),
|
|
25101
25800
|
deadline
|
|
@@ -25121,6 +25820,8 @@ var LockpickClient = class {
|
|
|
25121
25820
|
} finally {
|
|
25122
25821
|
clearTimeout(timeout);
|
|
25123
25822
|
}
|
|
25823
|
+
const redirect = hostRedirect(this.host, url3, response);
|
|
25824
|
+
if (redirect) throw redirect;
|
|
25124
25825
|
if (!envelope || typeof envelope !== "object" || !("data" in envelope))
|
|
25125
25826
|
throw new LockpickApiError(
|
|
25126
25827
|
`${this.host} did not answer as Lockpick (HTTP ${response.status}). Check the URL, and that CLI access is enabled there.`,
|
|
@@ -25143,7 +25844,7 @@ var LockpickClient = class {
|
|
|
25143
25844
|
response.status,
|
|
25144
25845
|
"CONTRACT_MISMATCH"
|
|
25145
25846
|
);
|
|
25146
|
-
return parsed.data;
|
|
25847
|
+
return neutralizeDeep(parsed.data);
|
|
25147
25848
|
}
|
|
25148
25849
|
startDeviceLogin(client) {
|
|
25149
25850
|
return this.request(
|
|
@@ -25627,6 +26328,11 @@ async function clientFor(options = {}) {
|
|
|
25627
26328
|
}
|
|
25628
26329
|
|
|
25629
26330
|
// src/doctor.ts
|
|
26331
|
+
var CAPABILITY_GAP = {
|
|
26332
|
+
pulse: "your role on every site here only lets you read results.",
|
|
26333
|
+
sites: "your workspace role cannot add sites.",
|
|
26334
|
+
deep: "Deep is not available to you here (role, early access or server setting)."
|
|
26335
|
+
};
|
|
25630
26336
|
async function runDoctor(options = {}) {
|
|
25631
26337
|
const checks = [];
|
|
25632
26338
|
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
@@ -25672,14 +26378,22 @@ async function runDoctor(options = {}) {
|
|
|
25672
26378
|
}
|
|
25673
26379
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
25674
26380
|
try {
|
|
25675
|
-
const
|
|
25676
|
-
|
|
25677
|
-
|
|
25678
|
-
|
|
25679
|
-
|
|
25680
|
-
|
|
25681
|
-
|
|
25682
|
-
)
|
|
26381
|
+
const health = new URL("/api/health/ready", session.host);
|
|
26382
|
+
const response = await fetchImpl(health, {
|
|
26383
|
+
method: "GET",
|
|
26384
|
+
redirect: "manual",
|
|
26385
|
+
signal: AbortSignal.timeout(1e4)
|
|
26386
|
+
});
|
|
26387
|
+
const redirect = hostRedirect(session.host, health, response);
|
|
26388
|
+
if (redirect) {
|
|
26389
|
+
checks.push({
|
|
26390
|
+
status: "FAIL",
|
|
26391
|
+
name: "Connection",
|
|
26392
|
+
detail: redirect.message,
|
|
26393
|
+
nextStep: redirect.canonicalHost ? `Run lockpick login --host ${redirect.canonicalHost}.` : "Check the host, then rerun doctor."
|
|
26394
|
+
});
|
|
26395
|
+
return checks;
|
|
26396
|
+
}
|
|
25683
26397
|
checks.push(
|
|
25684
26398
|
response.ok ? {
|
|
25685
26399
|
status: "PASS",
|
|
@@ -25718,8 +26432,9 @@ async function runDoctor(options = {}) {
|
|
|
25718
26432
|
name: "Authentication",
|
|
25719
26433
|
detail: `${me.user.name} in ${me.workspace.name} (${me.workspace.id}); key ${me.key.prefix}\u2026`
|
|
25720
26434
|
});
|
|
26435
|
+
let capabilities = null;
|
|
25721
26436
|
try {
|
|
25722
|
-
await client.agent.whoami();
|
|
26437
|
+
({ capabilities } = await client.agent.whoami());
|
|
25723
26438
|
checks.push({
|
|
25724
26439
|
status: "PASS",
|
|
25725
26440
|
name: "Agent API",
|
|
@@ -25739,12 +26454,29 @@ async function runDoctor(options = {}) {
|
|
|
25739
26454
|
name: "Scope",
|
|
25740
26455
|
detail: `The key lacks ${options.scope} permission.`,
|
|
25741
26456
|
nextStep: `Run lockpick login --host ${session.host} --${options.scope} and approve it in the browser.`
|
|
26457
|
+
} : options.scope && capabilities && !capabilities[options.scope] ? {
|
|
26458
|
+
// The key may, but the person's role (or the server) may not:
|
|
26459
|
+
// a project viewer's Pulse key starts nothing.
|
|
26460
|
+
status: "WARN",
|
|
26461
|
+
name: "Scope",
|
|
26462
|
+
detail: `The key has ${options.scope} permission, but ${CAPABILITY_GAP[options.scope]}`,
|
|
26463
|
+
nextStep: "Ask a workspace admin or project manager for the contributor or manager role on a site."
|
|
25742
26464
|
} : {
|
|
25743
26465
|
status: "PASS",
|
|
25744
26466
|
name: "Scope",
|
|
25745
26467
|
detail: options.scope ? `${options.scope} permission is present.` : `Granted: ${me.key.scopes.join(", ")}.`
|
|
25746
26468
|
}
|
|
25747
26469
|
);
|
|
26470
|
+
const daysLeft = Math.floor(
|
|
26471
|
+
(Date.parse(me.key.expiresAt) - Date.now()) / 864e5
|
|
26472
|
+
);
|
|
26473
|
+
if (daysLeft < 7)
|
|
26474
|
+
checks.push({
|
|
26475
|
+
status: "WARN",
|
|
26476
|
+
name: "Key expiry",
|
|
26477
|
+
detail: `The key expires when idle on ${me.key.expiresAt.slice(0, 10)} (${Math.max(0, daysLeft)} days).`,
|
|
26478
|
+
nextStep: "Using it extends it; for CI, sign in again before then."
|
|
26479
|
+
});
|
|
25748
26480
|
} catch (error51) {
|
|
25749
26481
|
const apiError = error51 instanceof LockpickApiError ? error51 : null;
|
|
25750
26482
|
checks.push({
|
|
@@ -25766,7 +26498,9 @@ var EXIT = {
|
|
|
25766
26498
|
permission: 4,
|
|
25767
26499
|
validation: 5,
|
|
25768
26500
|
rateLimit: 6,
|
|
25769
|
-
provider: 7
|
|
26501
|
+
provider: 7,
|
|
26502
|
+
/** A finished scan has open findings at or above `--fail-on`. */
|
|
26503
|
+
findings: 8
|
|
25770
26504
|
};
|
|
25771
26505
|
function exitCodeFor(error51) {
|
|
25772
26506
|
if (!(error51 instanceof LockpickApiError)) return EXIT.error;
|
|
@@ -25800,7 +26534,8 @@ var CODES = {
|
|
|
25800
26534
|
cyan: [36, 39]
|
|
25801
26535
|
};
|
|
25802
26536
|
function painter(stream = process.stdout, env = process.env) {
|
|
25803
|
-
const
|
|
26537
|
+
const forced = env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0";
|
|
26538
|
+
const enabled = !env.NO_COLOR && env.FORCE_COLOR !== "0" && (forced || Boolean(stream.isTTY) && env.TERM !== "dumb");
|
|
25804
26539
|
return (style, text2) => {
|
|
25805
26540
|
if (!enabled) return text2;
|
|
25806
26541
|
const [open4, close] = CODES[style];
|
|
@@ -25830,17 +26565,33 @@ function plural(count, noun) {
|
|
|
25830
26565
|
function date5(value) {
|
|
25831
26566
|
return value ? value.replace("T", " ").slice(0, 16) : "\u2014";
|
|
25832
26567
|
}
|
|
25833
|
-
|
|
25834
|
-
|
|
26568
|
+
var STYLE = /\u001b\[\d+m/g;
|
|
26569
|
+
var visible = (text2) => text2.replace(STYLE, "").length;
|
|
26570
|
+
function clip(text2, max) {
|
|
26571
|
+
if (visible(text2) <= max) return text2;
|
|
26572
|
+
return `${text2.replace(STYLE, "").slice(0, Math.max(1, max - 1))}\u2026`;
|
|
26573
|
+
}
|
|
26574
|
+
function table(rows, width2, flex = rows[0].length - 1) {
|
|
25835
26575
|
const widths = rows[0].map(
|
|
25836
26576
|
(_, column) => Math.max(...rows.map((row) => visible(row[column] ?? "")))
|
|
25837
26577
|
);
|
|
26578
|
+
if (width2) {
|
|
26579
|
+
const others = widths.reduce(
|
|
26580
|
+
(sum, columnWidth, column) => column === flex ? sum : sum + columnWidth + 2,
|
|
26581
|
+
0
|
|
26582
|
+
);
|
|
26583
|
+
widths[flex] = Math.min(widths[flex], Math.max(16, width2 - others - 1));
|
|
26584
|
+
}
|
|
25838
26585
|
return rows.map(
|
|
25839
|
-
(row) => row.map(
|
|
25840
|
-
|
|
25841
|
-
|
|
26586
|
+
(row) => row.map((cell, column) => {
|
|
26587
|
+
const fitted = column === flex ? clip(cell, widths[column]) : cell;
|
|
26588
|
+
return column === row.length - 1 ? fitted : fitted + " ".repeat(Math.max(0, widths[column] - visible(fitted)));
|
|
26589
|
+
}).join(" ")
|
|
25842
26590
|
).join("\n");
|
|
25843
26591
|
}
|
|
26592
|
+
function next(paint2, ...commands) {
|
|
26593
|
+
return paint2("dim", `Next: ${commands.join(" \xB7 ")}`);
|
|
26594
|
+
}
|
|
25844
26595
|
function runKindLabel(run2) {
|
|
25845
26596
|
if (run2.kind === "pentest")
|
|
25846
26597
|
return run2.mode ? `pentest/${run2.mode}` : "pentest";
|
|
@@ -25848,35 +26599,51 @@ function runKindLabel(run2) {
|
|
|
25848
26599
|
if (run2.assessmentLevel === "standard") return "Standard";
|
|
25849
26600
|
return "Pulse";
|
|
25850
26601
|
}
|
|
25851
|
-
function formatErrorLines(message, agentError) {
|
|
26602
|
+
function formatErrorLines(message, agentError, docsUrl2) {
|
|
25852
26603
|
const lines = [message];
|
|
25853
26604
|
if (agentError?.nextStep) lines.push(agentError.nextStep);
|
|
25854
26605
|
if (agentError?.retryAfterSeconds !== null && agentError?.retryAfterSeconds !== void 0)
|
|
25855
26606
|
lines.push(`Retry after ${agentError.retryAfterSeconds} seconds.`);
|
|
25856
26607
|
if (agentError?.webUrl) lines.push(agentError.webUrl);
|
|
26608
|
+
if (docsUrl2) lines.push(`Help: ${docsUrl2}`);
|
|
25857
26609
|
return lines;
|
|
25858
26610
|
}
|
|
25859
26611
|
function runLine(run2, paint2) {
|
|
25860
26612
|
if (!run2) return paint2("dim", "none yet");
|
|
25861
26613
|
return `${statusColor(run2.status, paint2)} ${paint2("dim", `${date5(run2.createdAt)} \xB7 ${run2.id}`)}`;
|
|
25862
26614
|
}
|
|
25863
|
-
function formatProjects(projects2, paint2 = plain) {
|
|
26615
|
+
function formatProjects(projects2, paint2 = plain, width2) {
|
|
25864
26616
|
if (projects2.length === 0)
|
|
25865
|
-
return
|
|
25866
|
-
|
|
26617
|
+
return [
|
|
26618
|
+
"No projects you can read in this workspace.",
|
|
26619
|
+
paint2(
|
|
26620
|
+
"dim",
|
|
26621
|
+
"Add one with `lockpick sites add <url>`, or ask a workspace admin to grant you a project."
|
|
26622
|
+
)
|
|
26623
|
+
].join("\n");
|
|
26624
|
+
const rows = table(
|
|
25867
26625
|
[
|
|
25868
|
-
|
|
25869
|
-
|
|
25870
|
-
|
|
25871
|
-
|
|
26626
|
+
[
|
|
26627
|
+
paint2("bold", "PROJECT"),
|
|
26628
|
+
paint2("bold", "ID"),
|
|
26629
|
+
paint2("bold", "PULSE"),
|
|
26630
|
+
paint2("bold", "PENTEST")
|
|
26631
|
+
],
|
|
26632
|
+
...projects2.map((project) => [
|
|
26633
|
+
project.name,
|
|
26634
|
+
paint2("dim", project.id),
|
|
26635
|
+
project.latestPulse ? statusColor(project.latestPulse.status, paint2) : paint2("dim", "\u2014"),
|
|
26636
|
+
project.latestPentest ? statusColor(project.latestPentest.status, paint2) : paint2("dim", "\u2014")
|
|
26637
|
+
])
|
|
25872
26638
|
],
|
|
25873
|
-
|
|
25874
|
-
|
|
25875
|
-
|
|
25876
|
-
|
|
25877
|
-
|
|
25878
|
-
|
|
25879
|
-
|
|
26639
|
+
width2,
|
|
26640
|
+
0
|
|
26641
|
+
);
|
|
26642
|
+
return [
|
|
26643
|
+
rows,
|
|
26644
|
+
"",
|
|
26645
|
+
next(paint2, "lockpick status <project>", "lockpick pulse <project>")
|
|
26646
|
+
].join("\n");
|
|
25880
26647
|
}
|
|
25881
26648
|
function runBlock(label, run2, paint2, link) {
|
|
25882
26649
|
if (!run2) return [`${paint2("bold", label)} ${paint2("dim", "none yet")}`];
|
|
@@ -25919,24 +26686,27 @@ function formatStatus(status2, link, paint2 = plain) {
|
|
|
25919
26686
|
lines.push("", ...status2.caveats.map((caveat) => paint2("dim", caveat)));
|
|
25920
26687
|
return lines.filter((line, index) => line !== "" || lines[index - 1] !== "").join("\n");
|
|
25921
26688
|
}
|
|
25922
|
-
function formatRuns(runs2, paint2 = plain) {
|
|
26689
|
+
function formatRuns(runs2, paint2 = plain, width2) {
|
|
25923
26690
|
if (runs2.length === 0) return "No runs match.";
|
|
25924
|
-
return table(
|
|
26691
|
+
return table(
|
|
25925
26692
|
[
|
|
25926
|
-
|
|
25927
|
-
|
|
25928
|
-
|
|
25929
|
-
|
|
25930
|
-
|
|
26693
|
+
[
|
|
26694
|
+
paint2("bold", "RUN"),
|
|
26695
|
+
paint2("bold", "KIND"),
|
|
26696
|
+
paint2("bold", "STATUS"),
|
|
26697
|
+
paint2("bold", "CREATED"),
|
|
26698
|
+
paint2("bold", "TARGET")
|
|
26699
|
+
],
|
|
26700
|
+
...runs2.map((run2) => [
|
|
26701
|
+
run2.id,
|
|
26702
|
+
runKindLabel(run2),
|
|
26703
|
+
statusColor(run2.status, paint2),
|
|
26704
|
+
date5(run2.createdAt),
|
|
26705
|
+
run2.target
|
|
26706
|
+
])
|
|
25931
26707
|
],
|
|
25932
|
-
|
|
25933
|
-
|
|
25934
|
-
runKindLabel(run2),
|
|
25935
|
-
statusColor(run2.status, paint2),
|
|
25936
|
-
date5(run2.createdAt),
|
|
25937
|
-
run2.target
|
|
25938
|
-
])
|
|
25939
|
-
]);
|
|
26708
|
+
width2
|
|
26709
|
+
);
|
|
25940
26710
|
}
|
|
25941
26711
|
function formatRun(run2, link, paint2 = plain) {
|
|
25942
26712
|
const lines = [
|
|
@@ -25957,7 +26727,25 @@ function formatRun(run2, link, paint2 = plain) {
|
|
|
25957
26727
|
"Limits (anything outside them is unknown, not passing):",
|
|
25958
26728
|
...run2.coverage.limitations.map((item) => ` - ${item}`)
|
|
25959
26729
|
);
|
|
26730
|
+
if (run2.exposureCheck) {
|
|
26731
|
+
const [headline, ...limits] = exposureCheckLines(run2.exposureCheck);
|
|
26732
|
+
lines.push(
|
|
26733
|
+
`Exposure ${headline}`,
|
|
26734
|
+
...limits.map((item) => ` - ${item}`)
|
|
26735
|
+
);
|
|
26736
|
+
}
|
|
25960
26737
|
lines.push(paint2("dim", link(run2.webPath)));
|
|
26738
|
+
const open4 = Object.values(run2.open).reduce((sum, count) => sum + count, 0);
|
|
26739
|
+
if (!CLI_TERMINAL_RUN_STATUSES.includes(run2.status))
|
|
26740
|
+
lines.push(next(paint2, `lockpick watch ${run2.id}`));
|
|
26741
|
+
else if (run2.status === "succeeded" && open4 > 0)
|
|
26742
|
+
lines.push(
|
|
26743
|
+
next(
|
|
26744
|
+
paint2,
|
|
26745
|
+
`lockpick findings ${run2.id}`,
|
|
26746
|
+
`lockpick fix ${run2.id} --agent claude`
|
|
26747
|
+
)
|
|
26748
|
+
);
|
|
25961
26749
|
return lines.join("\n");
|
|
25962
26750
|
}
|
|
25963
26751
|
var SEVERITY_STYLE = {
|
|
@@ -25967,22 +26755,46 @@ var SEVERITY_STYLE = {
|
|
|
25967
26755
|
low: "dim",
|
|
25968
26756
|
info: "dim"
|
|
25969
26757
|
};
|
|
25970
|
-
function formatFindings(findings2, paint2 = plain) {
|
|
25971
|
-
if (findings2.length === 0)
|
|
25972
|
-
|
|
25973
|
-
|
|
25974
|
-
|
|
25975
|
-
|
|
25976
|
-
|
|
25977
|
-
|
|
25978
|
-
|
|
25979
|
-
|
|
25980
|
-
|
|
25981
|
-
|
|
25982
|
-
|
|
25983
|
-
|
|
25984
|
-
|
|
25985
|
-
|
|
26758
|
+
function formatFindings(findings2, paint2 = plain, width2) {
|
|
26759
|
+
if (findings2.length === 0)
|
|
26760
|
+
return paint2("green", "No findings need attention.");
|
|
26761
|
+
const bySeverity = CLI_SEVERITIES.map(
|
|
26762
|
+
(severity) => [
|
|
26763
|
+
severity,
|
|
26764
|
+
findings2.filter((finding2) => finding2.severity === severity).length
|
|
26765
|
+
]
|
|
26766
|
+
).filter(([, count]) => count > 0);
|
|
26767
|
+
const runId = findings2[0].runId;
|
|
26768
|
+
return [
|
|
26769
|
+
table(
|
|
26770
|
+
[
|
|
26771
|
+
[
|
|
26772
|
+
paint2("bold", "SEVERITY"),
|
|
26773
|
+
paint2("bold", "STATE"),
|
|
26774
|
+
paint2("bold", "TITLE"),
|
|
26775
|
+
paint2("bold", "FINDING")
|
|
26776
|
+
],
|
|
26777
|
+
...findings2.map((finding2) => [
|
|
26778
|
+
paint2(SEVERITY_STYLE[finding2.severity], finding2.severity),
|
|
26779
|
+
finding2.state,
|
|
26780
|
+
finding2.title,
|
|
26781
|
+
paint2("dim", finding2.id)
|
|
26782
|
+
])
|
|
26783
|
+
],
|
|
26784
|
+
// Titles give way first; a long id is never cut.
|
|
26785
|
+
width2,
|
|
26786
|
+
2
|
|
26787
|
+
),
|
|
26788
|
+
"",
|
|
26789
|
+
`${plural(findings2.length, "finding")}: ${bySeverity.map(
|
|
26790
|
+
([severity, count]) => paint2(SEVERITY_STYLE[severity], `${count} ${severity}`)
|
|
26791
|
+
).join(", ")}`,
|
|
26792
|
+
next(
|
|
26793
|
+
paint2,
|
|
26794
|
+
`lockpick finding ${runId} <finding>`,
|
|
26795
|
+
`lockpick brief ${runId}`
|
|
26796
|
+
)
|
|
26797
|
+
].join("\n");
|
|
25986
26798
|
}
|
|
25987
26799
|
function formatFinding(finding2, link, paint2 = plain) {
|
|
25988
26800
|
const lines = [
|
|
@@ -26318,6 +27130,250 @@ function formatRelevant(tags, paint2 = plain) {
|
|
|
26318
27130
|
}
|
|
26319
27131
|
return lines.join("\n");
|
|
26320
27132
|
}
|
|
27133
|
+
var DOCTOR_MARK = {
|
|
27134
|
+
PASS: ["green", "\u2714"],
|
|
27135
|
+
WARN: ["yellow", "!"],
|
|
27136
|
+
FAIL: ["red", "\u2716"]
|
|
27137
|
+
};
|
|
27138
|
+
function formatDoctor(checks, paint2 = plain, docsUrl2) {
|
|
27139
|
+
const width2 = Math.max(...checks.map((check3) => check3.name.length));
|
|
27140
|
+
const lines = checks.map((check3) => {
|
|
27141
|
+
const [style, mark] = DOCTOR_MARK[check3.status];
|
|
27142
|
+
return [
|
|
27143
|
+
`${paint2(style, `${mark} ${check3.status}`)} ${check3.name.padEnd(width2)} ${check3.detail}`,
|
|
27144
|
+
...check3.nextStep ? [`${" ".repeat(width2 + 10)}${paint2("dim", `\u2192 ${check3.nextStep}`)}`] : []
|
|
27145
|
+
].join("\n");
|
|
27146
|
+
});
|
|
27147
|
+
const failed = checks.filter((check3) => check3.status === "FAIL").length;
|
|
27148
|
+
const warned = checks.filter((check3) => check3.status === "WARN").length;
|
|
27149
|
+
lines.push(
|
|
27150
|
+
"",
|
|
27151
|
+
failed ? paint2("red", `${plural(failed, "check")} failed.`) : warned ? paint2("yellow", `Ready, with ${plural(warned, "warning")}.`) : paint2("green", "Everything checks out.")
|
|
27152
|
+
);
|
|
27153
|
+
if (docsUrl2 && (failed || warned))
|
|
27154
|
+
lines.push(paint2("dim", `Help: ${docsUrl2}`));
|
|
27155
|
+
return lines.join("\n");
|
|
27156
|
+
}
|
|
27157
|
+
function formatDocSearch(search, paint2 = plain) {
|
|
27158
|
+
if (search.results.length === 0)
|
|
27159
|
+
return `No Lockpick docs match "${search.query}".`;
|
|
27160
|
+
return search.results.map(
|
|
27161
|
+
(hit) => [
|
|
27162
|
+
`${paint2("bold", hit.title)}${hit.heading ? paint2("dim", ` \u203A ${hit.heading}`) : ""}`,
|
|
27163
|
+
` ${hit.snippet.replace(/\s+/g, " ").trim()}`,
|
|
27164
|
+
` ${paint2("cyan", hit.url)}`
|
|
27165
|
+
].join("\n")
|
|
27166
|
+
).join("\n\n");
|
|
27167
|
+
}
|
|
27168
|
+
|
|
27169
|
+
// src/docs.ts
|
|
27170
|
+
var DOCS = {
|
|
27171
|
+
cli: "/docs/cli/install",
|
|
27172
|
+
signIn: "/docs/cli/install#sign-in",
|
|
27173
|
+
exitCodes: "/docs/cli/install#exit-codes",
|
|
27174
|
+
commands: "/docs/cli/commands",
|
|
27175
|
+
configuration: "/docs/cli/configuration",
|
|
27176
|
+
ci: "/docs/cli/ci",
|
|
27177
|
+
permissions: "/docs/mcp/permissions",
|
|
27178
|
+
pulseOrDeep: "/docs/get-started/pulse-or-deep",
|
|
27179
|
+
fix: "/docs/get-started/fix-what-we-found",
|
|
27180
|
+
checkAgain: "/docs/get-started/check-again"
|
|
27181
|
+
};
|
|
27182
|
+
function docsPageForError(code) {
|
|
27183
|
+
switch (code) {
|
|
27184
|
+
case "UNAUTHENTICATED":
|
|
27185
|
+
case "NO_HOST":
|
|
27186
|
+
return "signIn";
|
|
27187
|
+
case "INSUFFICIENT_SCOPE":
|
|
27188
|
+
case "FORBIDDEN":
|
|
27189
|
+
return "permissions";
|
|
27190
|
+
case "ENTITLEMENT_REQUIRED":
|
|
27191
|
+
case "FEATURE_UNAVAILABLE":
|
|
27192
|
+
case "PROOF_REQUIRED":
|
|
27193
|
+
return "pulseOrDeep";
|
|
27194
|
+
case "NOT_LOCKPICK":
|
|
27195
|
+
case "CONTRACT_MISMATCH":
|
|
27196
|
+
case "NETWORK":
|
|
27197
|
+
case "TIMEOUT":
|
|
27198
|
+
return "configuration";
|
|
27199
|
+
case "VALIDATION_ERROR":
|
|
27200
|
+
case "INVALID_INPUT":
|
|
27201
|
+
return "commands";
|
|
27202
|
+
default:
|
|
27203
|
+
return null;
|
|
27204
|
+
}
|
|
27205
|
+
}
|
|
27206
|
+
function docsUrl(host, page) {
|
|
27207
|
+
if (!host) return null;
|
|
27208
|
+
try {
|
|
27209
|
+
return new URL(DOCS[page], host).toString();
|
|
27210
|
+
} catch {
|
|
27211
|
+
return null;
|
|
27212
|
+
}
|
|
27213
|
+
}
|
|
27214
|
+
|
|
27215
|
+
// src/ci.ts
|
|
27216
|
+
var FAIL_ON = ["critical", "high", "medium", "low"];
|
|
27217
|
+
function failOnArg(value) {
|
|
27218
|
+
if (value === void 0) return void 0;
|
|
27219
|
+
if (FAIL_ON.includes(value)) return value;
|
|
27220
|
+
throw new LockpickApiError(
|
|
27221
|
+
`--fail-on must be one of: ${FAIL_ON.join(", ")}`,
|
|
27222
|
+
400,
|
|
27223
|
+
"VALIDATION_ERROR"
|
|
27224
|
+
);
|
|
27225
|
+
}
|
|
27226
|
+
function countAtOrAbove(open4, threshold) {
|
|
27227
|
+
return FAIL_ON.slice(0, FAIL_ON.indexOf(threshold) + 1).reduce(
|
|
27228
|
+
(sum, severity) => sum + open4[severity],
|
|
27229
|
+
0
|
|
27230
|
+
);
|
|
27231
|
+
}
|
|
27232
|
+
function openCounts(findings2) {
|
|
27233
|
+
const counts2 = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
27234
|
+
for (const finding2 of findings2)
|
|
27235
|
+
if (finding2.severity !== "info") counts2[finding2.severity]++;
|
|
27236
|
+
return counts2;
|
|
27237
|
+
}
|
|
27238
|
+
var LOCKPICK_ACTION = {
|
|
27239
|
+
ref: "promptadvisers/lockpick-action@v1",
|
|
27240
|
+
inputs: {
|
|
27241
|
+
host: "host",
|
|
27242
|
+
site: "site",
|
|
27243
|
+
key: "key",
|
|
27244
|
+
failOn: "fail-on"
|
|
27245
|
+
}
|
|
27246
|
+
};
|
|
27247
|
+
var CI_KEY_SECRET = "LOCKPICK_KEY";
|
|
27248
|
+
var CI_WORKFLOW_FILE = ".github/workflows/lockpick.yml";
|
|
27249
|
+
var SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
27250
|
+
function githubWorkflow(options) {
|
|
27251
|
+
if (!SAFE_ID.test(options.siteId))
|
|
27252
|
+
throw new LockpickApiError(
|
|
27253
|
+
`Site id "${options.siteId}" cannot be written into a workflow.`,
|
|
27254
|
+
400,
|
|
27255
|
+
"VALIDATION_ERROR"
|
|
27256
|
+
);
|
|
27257
|
+
const host = new URL(options.host).origin;
|
|
27258
|
+
const name = options.siteName.replace(/[^\p{L}\p{N} ._-]/gu, "").trim();
|
|
27259
|
+
const step = options.action ? [
|
|
27260
|
+
` - name: Pulse check of ${name || options.siteId}`,
|
|
27261
|
+
` uses: ${options.actionRef ?? LOCKPICK_ACTION.ref}`,
|
|
27262
|
+
` with:`,
|
|
27263
|
+
` ${LOCKPICK_ACTION.inputs.host}: '${host}'`,
|
|
27264
|
+
` ${LOCKPICK_ACTION.inputs.site}: '${options.siteId}'`,
|
|
27265
|
+
` ${LOCKPICK_ACTION.inputs.key}: \${{ secrets.${CI_KEY_SECRET} }}`,
|
|
27266
|
+
` ${LOCKPICK_ACTION.inputs.failOn}: '${options.failOn}'`
|
|
27267
|
+
] : [
|
|
27268
|
+
` - uses: actions/setup-node@v4`,
|
|
27269
|
+
` with:`,
|
|
27270
|
+
` node-version: 22`,
|
|
27271
|
+
` - name: Pulse check of ${name || options.siteId}`,
|
|
27272
|
+
` env:`,
|
|
27273
|
+
` LOCKPICK_HOST: '${host}'`,
|
|
27274
|
+
` LOCKPICK_KEY: \${{ secrets.${CI_KEY_SECRET} }}`,
|
|
27275
|
+
` run: npx -y lockpicks@${options.cliVersion} pulse ${options.siteId} --no-lockfile --fail-on ${options.failOn} --timeout 900`
|
|
27276
|
+
];
|
|
27277
|
+
return [
|
|
27278
|
+
`# Lockpick Pulse check, written by \`lockpick ci init\`.`,
|
|
27279
|
+
`# Each run uses one Pulse from your allowance (or 1 credit), so it runs`,
|
|
27280
|
+
`# when you publish a release and when you start it by hand.`,
|
|
27281
|
+
`# It fails when open findings are ${options.failOn} or more severe.`,
|
|
27282
|
+
...options.docsUrl ? [`# Docs: ${options.docsUrl}`] : [],
|
|
27283
|
+
`name: Lockpick`,
|
|
27284
|
+
`on:`,
|
|
27285
|
+
` release:`,
|
|
27286
|
+
` types: [published]`,
|
|
27287
|
+
` workflow_dispatch:`,
|
|
27288
|
+
`permissions:`,
|
|
27289
|
+
` contents: read`,
|
|
27290
|
+
`concurrency:`,
|
|
27291
|
+
` group: lockpick-${options.siteId}`,
|
|
27292
|
+
` cancel-in-progress: false`,
|
|
27293
|
+
`jobs:`,
|
|
27294
|
+
` pulse:`,
|
|
27295
|
+
` name: Lockpick Pulse`,
|
|
27296
|
+
` runs-on: ubuntu-latest`,
|
|
27297
|
+
` timeout-minutes: 20`,
|
|
27298
|
+
` steps:`,
|
|
27299
|
+
...step,
|
|
27300
|
+
""
|
|
27301
|
+
].join("\n");
|
|
27302
|
+
}
|
|
27303
|
+
function isPrivateHost(host) {
|
|
27304
|
+
const { hostname: hostname4 } = new URL(host);
|
|
27305
|
+
return hostname4 === "localhost" || hostname4.endsWith(".local") || hostname4.endsWith(".ts.net") || /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/.test(
|
|
27306
|
+
hostname4
|
|
27307
|
+
) || hostname4 === "[::1]" || !hostname4.includes(".");
|
|
27308
|
+
}
|
|
27309
|
+
|
|
27310
|
+
// src/progress.ts
|
|
27311
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
27312
|
+
function elapsed(ms) {
|
|
27313
|
+
const seconds = Math.floor(ms / 1e3);
|
|
27314
|
+
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
|
|
27315
|
+
}
|
|
27316
|
+
function bar(percent) {
|
|
27317
|
+
const filled = Math.round(Math.min(100, Math.max(0, percent)) / 10);
|
|
27318
|
+
return `${"\u2588".repeat(filled)}${"\u2591".repeat(10 - filled)} ${percent}%`;
|
|
27319
|
+
}
|
|
27320
|
+
function progress(paint2, options = {}) {
|
|
27321
|
+
const stream = options.stream ?? process.stderr;
|
|
27322
|
+
const now = options.now ?? Date.now;
|
|
27323
|
+
const env = options.env ?? process.env;
|
|
27324
|
+
const started = now();
|
|
27325
|
+
let text2 = "Waiting";
|
|
27326
|
+
let percent = null;
|
|
27327
|
+
if (options.quiet) return { update: () => {
|
|
27328
|
+
}, stop: () => {
|
|
27329
|
+
} };
|
|
27330
|
+
const live = Boolean(stream.isTTY) && env.TERM !== "dumb" && !env.CI;
|
|
27331
|
+
if (!live) {
|
|
27332
|
+
let last = "";
|
|
27333
|
+
return {
|
|
27334
|
+
update: (next2, nextPercent) => {
|
|
27335
|
+
const line = `${next2}${nextPercent != null ? ` (${nextPercent}%)` : ""}`;
|
|
27336
|
+
if (line !== last)
|
|
27337
|
+
stream.write(
|
|
27338
|
+
`${terminalSafe(`${new Date(now()).toISOString().slice(11, 19)} ${line}`)}
|
|
27339
|
+
`
|
|
27340
|
+
);
|
|
27341
|
+
last = line;
|
|
27342
|
+
},
|
|
27343
|
+
stop: (final) => {
|
|
27344
|
+
if (final) stream.write(`${terminalSafe(final)}
|
|
27345
|
+
`);
|
|
27346
|
+
}
|
|
27347
|
+
};
|
|
27348
|
+
}
|
|
27349
|
+
let frame = 0;
|
|
27350
|
+
const draw = () => {
|
|
27351
|
+
const room = Math.max(10, (stream.columns ?? 80) - 32);
|
|
27352
|
+
const parts = [
|
|
27353
|
+
paint2("cyan", FRAMES[frame++ % FRAMES.length]),
|
|
27354
|
+
text2.length > room ? `${text2.slice(0, room - 1)}\u2026` : text2,
|
|
27355
|
+
percent != null ? paint2("dim", bar(percent)) : "",
|
|
27356
|
+
paint2("dim", elapsed(now() - started))
|
|
27357
|
+
].filter(Boolean);
|
|
27358
|
+
const line = terminalSafe(parts.join(" "));
|
|
27359
|
+
stream.write(`\r\x1B[2K${line}`);
|
|
27360
|
+
};
|
|
27361
|
+
const timer = setInterval(draw, 100);
|
|
27362
|
+
timer.unref?.();
|
|
27363
|
+
return {
|
|
27364
|
+
update: (next2, nextPercent) => {
|
|
27365
|
+
text2 = next2;
|
|
27366
|
+
percent = nextPercent ?? null;
|
|
27367
|
+
draw();
|
|
27368
|
+
},
|
|
27369
|
+
stop: (final) => {
|
|
27370
|
+
clearInterval(timer);
|
|
27371
|
+
stream.write("\r\x1B[2K");
|
|
27372
|
+
if (final) stream.write(`${terminalSafe(final)}
|
|
27373
|
+
`);
|
|
27374
|
+
}
|
|
27375
|
+
};
|
|
27376
|
+
}
|
|
26321
27377
|
|
|
26322
27378
|
// src/login.ts
|
|
26323
27379
|
var SCOPE_ACCESS = {
|
|
@@ -26422,7 +27478,7 @@ import { delimiter, join as join3, sep } from "node:path";
|
|
|
26422
27478
|
import { createInterface } from "node:readline/promises";
|
|
26423
27479
|
|
|
26424
27480
|
// src/version.ts
|
|
26425
|
-
var CLI_VERSION = true ? "0.
|
|
27481
|
+
var CLI_VERSION = true ? "0.4.0" : "0.0.0-dev";
|
|
26426
27482
|
var CLI_PACKAGE = true ? "lockpicks" : "lockpicks";
|
|
26427
27483
|
|
|
26428
27484
|
// src/install.ts
|
|
@@ -34990,6 +36046,13 @@ function renderRun(run2, link) {
|
|
|
34990
36046
|
"- Limits (anything outside them is unknown, not passing):",
|
|
34991
36047
|
...run2.coverage.limitations.map((item) => ` - ${item}`)
|
|
34992
36048
|
);
|
|
36049
|
+
if (run2.exposureCheck) {
|
|
36050
|
+
const [headline, ...limits] = exposureCheckLines(run2.exposureCheck);
|
|
36051
|
+
lines.push(
|
|
36052
|
+
`- Exposure check: ${headline}`,
|
|
36053
|
+
...limits.map((item) => ` - ${item}`)
|
|
36054
|
+
);
|
|
36055
|
+
}
|
|
34993
36056
|
lines.push(`- Web: ${link(run2.webPath)}`);
|
|
34994
36057
|
return lines.join("\n");
|
|
34995
36058
|
}
|
|
@@ -35168,9 +36231,72 @@ function levelName(scan) {
|
|
|
35168
36231
|
const level = scan.assessmentLevel ?? "pulse";
|
|
35169
36232
|
return level === "deep" ? "Deep" : level === "standard" ? "Standard" : "Pulse";
|
|
35170
36233
|
}
|
|
36234
|
+
var OUTCOME_REASON = {
|
|
36235
|
+
"no-address": "no DNS address",
|
|
36236
|
+
"dns-error": "the DNS lookup failed",
|
|
36237
|
+
"private-address": "it points to a private network address",
|
|
36238
|
+
refused: "the connection was refused",
|
|
36239
|
+
"no-answer": "the server did not answer in time",
|
|
36240
|
+
"connection-closed": "the server closed the connection",
|
|
36241
|
+
certificate: "the certificate failed the checks a browser makes",
|
|
36242
|
+
"old-tls": "only an old TLS version was offered",
|
|
36243
|
+
"large-headers": "the response headers were over the 64 KB Lockpick reads",
|
|
36244
|
+
"invalid-response": "the response was not valid HTTP",
|
|
36245
|
+
www: "the homepage redirects to its www address",
|
|
36246
|
+
"other-site": "the homepage redirects to another site",
|
|
36247
|
+
"sign-in": "the homepage redirects to a sign-in page",
|
|
36248
|
+
"redirect-loop": "the homepage redirects in a loop",
|
|
36249
|
+
redirect: "the homepage redirect is not one a run follows",
|
|
36250
|
+
"bot-check": "a bot check answered instead of the homepage",
|
|
36251
|
+
denied: "the site refused the request",
|
|
36252
|
+
"rate-limited": "the site asked Lockpick to slow down",
|
|
36253
|
+
"not-found": "the homepage answered not found",
|
|
36254
|
+
gone: "the homepage answered gone",
|
|
36255
|
+
"client-error": "the homepage answered with an error",
|
|
36256
|
+
"server-error": "the server answered with an error",
|
|
36257
|
+
maintenance: "the site is down for maintenance",
|
|
36258
|
+
parked: "the homepage is a parked domain page",
|
|
36259
|
+
empty: "the homepage is empty",
|
|
36260
|
+
"not-html": "the homepage is not an HTML page",
|
|
36261
|
+
"app-shell": "the homepage is an app shell built in the browser",
|
|
36262
|
+
interrupted: "the connection closed while the homepage loaded",
|
|
36263
|
+
"not-delivered": "the run stopped on Lockpick's side before its report was ready"
|
|
36264
|
+
};
|
|
36265
|
+
var VENDOR_CHECK = {
|
|
36266
|
+
cloudflare: "blocked by Cloudflare's bot check",
|
|
36267
|
+
vercel: "blocked by Vercel's bot check",
|
|
36268
|
+
other: "blocked by a firewall"
|
|
36269
|
+
};
|
|
36270
|
+
var OUTCOME_LEVEL = {
|
|
36271
|
+
"headers-only": "headers only",
|
|
36272
|
+
blocked: "blocked",
|
|
36273
|
+
transport: "no secure connection",
|
|
36274
|
+
unreachable: "unreachable",
|
|
36275
|
+
"terminal-failure": "did not finish"
|
|
36276
|
+
};
|
|
36277
|
+
var COVERAGE_TEXT = {
|
|
36278
|
+
page: "pages, their scripts and public records",
|
|
36279
|
+
"headers-and-records": "response headers and public records",
|
|
36280
|
+
"records-only": "public records only",
|
|
36281
|
+
none: "nothing could be collected"
|
|
36282
|
+
};
|
|
36283
|
+
function outcomeText(outcome) {
|
|
36284
|
+
if (!outcome || outcome.level === "full") return null;
|
|
36285
|
+
if (outcome.level === "blocked" && outcome.reason === "bot-check")
|
|
36286
|
+
return VENDOR_CHECK[outcome.vendor ?? "other"];
|
|
36287
|
+
const reason = outcome.reason ? OUTCOME_REASON[outcome.reason] : null;
|
|
36288
|
+
const status2 = outcome.homepageStatus && outcome.reason !== "rate-limited" ? ` (HTTP ${outcome.homepageStatus})` : "";
|
|
36289
|
+
const contact = outcome.contacted === void 0 ? "" : outcome.contacted ? ", after contacting the site (it counted as a run)" : ", before contacting the site (it did not count as a run)";
|
|
36290
|
+
return `${OUTCOME_LEVEL[outcome.level]}${reason ? `, ${reason}${status2}` : ""}${contact}`;
|
|
36291
|
+
}
|
|
36292
|
+
var UNFINISHED = /* @__PURE__ */ new Set(["queued", "running", "cancelling"]);
|
|
35171
36293
|
function headlineText(scan) {
|
|
35172
36294
|
const { score, grade, letter } = scan.headline;
|
|
35173
|
-
if (score === null)
|
|
36295
|
+
if (score === null) {
|
|
36296
|
+
const outcome = outcomeText(scan.outcome);
|
|
36297
|
+
if (outcome) return `not graded, ${outcome}`;
|
|
36298
|
+
return UNFINISHED.has(scan.status) ? "no grade yet" : "not graded";
|
|
36299
|
+
}
|
|
35174
36300
|
return `${letter ?? ""}${letter ? ", " : ""}${grade ?? "ungraded"}, score ${score}/100`;
|
|
35175
36301
|
}
|
|
35176
36302
|
function scanLine(scan) {
|
|
@@ -35239,6 +36365,16 @@ function renderScan(scan) {
|
|
|
35239
36365
|
lines.push(
|
|
35240
36366
|
`- Progress: ${inlineText(scan.progress.message, 200)}${scan.progress.percent !== null ? ` (${scan.progress.percent}%)` : ""}`
|
|
35241
36367
|
);
|
|
36368
|
+
if (scan.outcome)
|
|
36369
|
+
lines.push(`- Based on: ${COVERAGE_TEXT[scan.outcome.coverage]}`);
|
|
36370
|
+
if (scan.outcome?.level === "blocked")
|
|
36371
|
+
lines.push(
|
|
36372
|
+
`- Lockpick never tries to get past a bot check or firewall. The owner can let it through: ${new URL("/docs/protect/firewall", scan.webUrl).href}`
|
|
36373
|
+
);
|
|
36374
|
+
if (scan.outcome?.redirectOrigin)
|
|
36375
|
+
lines.push(
|
|
36376
|
+
`- Check instead: ${codeSpan(scan.outcome.redirectOrigin)} (the address the homepage redirects to)`
|
|
36377
|
+
);
|
|
35242
36378
|
if (scan.coverage.percent !== null)
|
|
35243
36379
|
lines.push(`- Coverage: ${scan.coverage.percent}%`);
|
|
35244
36380
|
if (scan.coverage.limitations.length)
|
|
@@ -35246,6 +36382,13 @@ function renderScan(scan) {
|
|
|
35246
36382
|
"- Limits (anything outside them is unknown, not passing):",
|
|
35247
36383
|
...scan.coverage.limitations.slice(0, 10).map((item) => ` - ${inlineText(item, 300)}`)
|
|
35248
36384
|
);
|
|
36385
|
+
if (scan.exposureCheck) {
|
|
36386
|
+
const [headline, ...limits] = exposureCheckLines(scan.exposureCheck);
|
|
36387
|
+
lines.push(
|
|
36388
|
+
`- Exposure check: ${headline}`,
|
|
36389
|
+
...limits.map((item) => ` - ${item}`)
|
|
36390
|
+
);
|
|
36391
|
+
}
|
|
35249
36392
|
lines.push(`- Web: ${scan.webUrl}`);
|
|
35250
36393
|
if (scan.suggestedPollAfterSeconds !== null)
|
|
35251
36394
|
lines.push(
|
|
@@ -36433,12 +37576,12 @@ var startsPulse = {
|
|
|
36433
37576
|
function runResult(backend, run2, format3) {
|
|
36434
37577
|
if (format3 === "json") return json2(run2);
|
|
36435
37578
|
const body = renderRun(run2, (path) => backend.webUrl(path));
|
|
36436
|
-
const
|
|
37579
|
+
const next2 = CLI_TERMINAL_RUN_STATUSES.includes(run2.status) ? run2.baselineRunId && run2.status === "succeeded" ? `
|
|
36437
37580
|
|
|
36438
37581
|
This was a recheck of \`${run2.baselineRunId}\`. Call lockpick_compare_runs with runId \`${run2.id}\` to see what changed.` : "" : `
|
|
36439
37582
|
|
|
36440
37583
|
Still ${run2.status}. Call lockpick_wait_for_run with runId \`${run2.id}\` to keep waiting.`;
|
|
36441
|
-
return text(body +
|
|
37584
|
+
return text(body + next2);
|
|
36442
37585
|
}
|
|
36443
37586
|
var whoamiTool = defineTool({
|
|
36444
37587
|
name: "lockpick_whoami",
|
|
@@ -36879,9 +38022,9 @@ function pollSeconds(value) {
|
|
|
36879
38022
|
);
|
|
36880
38023
|
return seconds;
|
|
36881
38024
|
}
|
|
36882
|
-
function progressLine(run2) {
|
|
38025
|
+
function progressLine(run2, withPercent = true) {
|
|
36883
38026
|
const message = run2.progress?.message ? ` \xB7 ${run2.progress.message}` : "";
|
|
36884
|
-
const percent = run2.progress?.percent != null ? ` (${run2.progress.percent}%)` : "";
|
|
38027
|
+
const percent = withPercent && run2.progress?.percent != null ? ` (${run2.progress.percent}%)` : "";
|
|
36885
38028
|
return `${run2.status}${message}${percent}`;
|
|
36886
38029
|
}
|
|
36887
38030
|
|
|
@@ -37118,28 +38261,47 @@ var common = {
|
|
|
37118
38261
|
}
|
|
37119
38262
|
};
|
|
37120
38263
|
var paint = painter();
|
|
37121
|
-
var
|
|
38264
|
+
var epaint = painter(process.stderr);
|
|
38265
|
+
var out = (text2) => void process.stdout.write(`${terminalSafe(text2)}
|
|
37122
38266
|
`);
|
|
37123
|
-
var err = (text2) => void process.stderr.write(`${text2}
|
|
38267
|
+
var err = (text2) => void process.stderr.write(`${terminalSafe(text2)}
|
|
37124
38268
|
`);
|
|
38269
|
+
var width = () => terminalWidth(process.stdout);
|
|
37125
38270
|
var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
37126
38271
|
process.stdout.on("error", (error51) => {
|
|
37127
38272
|
if (error51.code === "EPIPE") process.exit(EXIT.ok);
|
|
37128
38273
|
throw error51;
|
|
37129
38274
|
});
|
|
38275
|
+
async function knownHost() {
|
|
38276
|
+
const flag = process.argv.indexOf("--host");
|
|
38277
|
+
const explicit = (flag > -1 ? process.argv[flag + 1] : void 0) ?? process.argv.find((arg) => arg.startsWith("--host="))?.slice(7) ?? process.env.LOCKPICK_HOST;
|
|
38278
|
+
try {
|
|
38279
|
+
if (explicit) return normalizeHost(explicit);
|
|
38280
|
+
return (await new CredentialStore().profile())?.host ?? null;
|
|
38281
|
+
} catch {
|
|
38282
|
+
return null;
|
|
38283
|
+
}
|
|
38284
|
+
}
|
|
38285
|
+
var docsLink = async (page) => docsUrl(await knownHost(), page);
|
|
37130
38286
|
async function action(work) {
|
|
37131
38287
|
try {
|
|
37132
38288
|
await work();
|
|
37133
38289
|
} catch (error51) {
|
|
37134
38290
|
const apiError = error51 instanceof LockpickApiError ? error51 : null;
|
|
37135
|
-
const message =
|
|
38291
|
+
const message = neutralize(
|
|
38292
|
+
error51 instanceof Error ? error51.message : String(error51)
|
|
38293
|
+
);
|
|
37136
38294
|
process.exitCode = exitCodeFor(error51);
|
|
37137
38295
|
const agentError = apiError?.agentError ?? null;
|
|
38296
|
+
const code = agentError?.code ?? apiError?.code ?? null;
|
|
38297
|
+
const page = docsPageForError(code);
|
|
38298
|
+
const help = page ? await docsLink(page) : null;
|
|
37138
38299
|
if (process.argv.includes("--json"))
|
|
37139
38300
|
err(
|
|
37140
38301
|
JSON.stringify({
|
|
37141
38302
|
error: agentError?.message ?? message,
|
|
37142
|
-
code
|
|
38303
|
+
code,
|
|
38304
|
+
...help ? { docsUrl: help } : {},
|
|
37143
38305
|
...agentError ? {
|
|
37144
38306
|
nextStep: agentError.nextStep,
|
|
37145
38307
|
retryAfterSeconds: agentError.retryAfterSeconds,
|
|
@@ -37151,10 +38313,11 @@ async function action(work) {
|
|
|
37151
38313
|
else {
|
|
37152
38314
|
const [first, ...rest] = formatErrorLines(
|
|
37153
38315
|
agentError?.message ?? message,
|
|
37154
|
-
agentError
|
|
38316
|
+
agentError,
|
|
38317
|
+
help
|
|
37155
38318
|
);
|
|
37156
|
-
err(
|
|
37157
|
-
for (const line of rest) err(line);
|
|
38319
|
+
err(epaint("red", `\u2716 ${first}`));
|
|
38320
|
+
for (const line of rest) err(` ${line}`);
|
|
37158
38321
|
}
|
|
37159
38322
|
}
|
|
37160
38323
|
}
|
|
@@ -37173,6 +38336,46 @@ function parse3(schema, value, name, allowed) {
|
|
|
37173
38336
|
);
|
|
37174
38337
|
}
|
|
37175
38338
|
}
|
|
38339
|
+
var NOT_IN_IDS = /[\u0000-\u001f\u007f\s]/;
|
|
38340
|
+
function idArg(value, name) {
|
|
38341
|
+
if (/^\.{1,2}$/.test(value) || NOT_IN_IDS.test(value) || !value || value.length > 300)
|
|
38342
|
+
throw new LockpickApiError(
|
|
38343
|
+
`${name} "${neutralize(value).slice(0, 60)}" is not a valid id. Copy it from \`lockpick runs\` or \`lockpick findings\`.`,
|
|
38344
|
+
400,
|
|
38345
|
+
"VALIDATION_ERROR"
|
|
38346
|
+
);
|
|
38347
|
+
return value;
|
|
38348
|
+
}
|
|
38349
|
+
function limitArg(value) {
|
|
38350
|
+
if (value === void 0) return void 0;
|
|
38351
|
+
const limit = Number(value);
|
|
38352
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
38353
|
+
throw new LockpickApiError(
|
|
38354
|
+
"--limit must be a whole number from 1 to 100.",
|
|
38355
|
+
400,
|
|
38356
|
+
"VALIDATION_ERROR"
|
|
38357
|
+
);
|
|
38358
|
+
return limit;
|
|
38359
|
+
}
|
|
38360
|
+
var failOnFlag = {
|
|
38361
|
+
failOn: {
|
|
38362
|
+
type: "string",
|
|
38363
|
+
description: `Exit 8 when open findings are this severe or worse: ${FAIL_ON.join(", ")} (for CI)`
|
|
38364
|
+
}
|
|
38365
|
+
};
|
|
38366
|
+
function gate(open4, threshold, json3) {
|
|
38367
|
+
if (!threshold) return;
|
|
38368
|
+
const count = countAtOrAbove(open4, threshold);
|
|
38369
|
+
if (count === 0) return;
|
|
38370
|
+
if (!json3)
|
|
38371
|
+
err(
|
|
38372
|
+
epaint(
|
|
38373
|
+
"red",
|
|
38374
|
+
`\u2716 ${count} open ${count === 1 ? "finding is" : "findings are"} ${threshold} or more severe (--fail-on ${threshold}).`
|
|
38375
|
+
)
|
|
38376
|
+
);
|
|
38377
|
+
process.exitCode = EXIT.findings;
|
|
38378
|
+
}
|
|
37176
38379
|
function agentArg(value) {
|
|
37177
38380
|
if (value === void 0) return void 0;
|
|
37178
38381
|
if (isPromptAgent(value)) return value;
|
|
@@ -37229,17 +38432,16 @@ var login = defineCommand({
|
|
|
37229
38432
|
run: ({ args }) => action(async () => {
|
|
37230
38433
|
const store = new CredentialStore();
|
|
37231
38434
|
const explicit = args.host ?? process.env.LOCKPICK_HOST;
|
|
37232
|
-
|
|
38435
|
+
let host = explicit ? normalizeHost(explicit) : (await store.profile())?.host;
|
|
37233
38436
|
if (!host)
|
|
37234
38437
|
throw new LockpickApiError(
|
|
37235
38438
|
"Which Lockpick? Run `lockpick login --host <url>`.",
|
|
37236
38439
|
400,
|
|
37237
38440
|
"NO_HOST"
|
|
37238
38441
|
);
|
|
37239
|
-
const client = new LockpickClient(host, null);
|
|
37240
38442
|
const requested = requestedScopes(args);
|
|
37241
|
-
const
|
|
37242
|
-
|
|
38443
|
+
const login2 = (at) => deviceLogin(
|
|
38444
|
+
new LockpickClient(at, null),
|
|
37243
38445
|
{
|
|
37244
38446
|
clientName: "Lockpick CLI",
|
|
37245
38447
|
hostname: hostname3(),
|
|
@@ -37248,8 +38450,20 @@ var login = defineCommand({
|
|
|
37248
38450
|
scopes: requested
|
|
37249
38451
|
},
|
|
37250
38452
|
{ print: args.json ? err : out, openBrowser, sleep },
|
|
37251
|
-
{ browser: args.browser, host }
|
|
38453
|
+
{ browser: args.browser, host: at }
|
|
37252
38454
|
);
|
|
38455
|
+
let approved;
|
|
38456
|
+
try {
|
|
38457
|
+
approved = await login2(host);
|
|
38458
|
+
} catch (error51) {
|
|
38459
|
+
if (!(error51 instanceof HostRedirectError) || !error51.canonicalHost)
|
|
38460
|
+
throw error51;
|
|
38461
|
+
(args.json ? err : out)(
|
|
38462
|
+
`${host} redirects to ${error51.canonicalHost}; signing in there.`
|
|
38463
|
+
);
|
|
38464
|
+
host = error51.canonicalHost;
|
|
38465
|
+
approved = await login2(host);
|
|
38466
|
+
}
|
|
37253
38467
|
const { inKeychain } = await store.save({
|
|
37254
38468
|
host,
|
|
37255
38469
|
workspace: approved.workspace,
|
|
@@ -37354,14 +38568,8 @@ var doctor = defineCommand({
|
|
|
37354
38568
|
host: args.host,
|
|
37355
38569
|
scope: args.scope
|
|
37356
38570
|
});
|
|
37357
|
-
|
|
37358
|
-
|
|
37359
|
-
checks,
|
|
37360
|
-
() => checks.map(
|
|
37361
|
-
(check3) => `[${check3.status}] ${check3.name}: ${check3.detail}${check3.nextStep ? `
|
|
37362
|
-
${check3.nextStep}` : ""}`
|
|
37363
|
-
).join("\n")
|
|
37364
|
-
);
|
|
38571
|
+
const help = await docsLink("cli");
|
|
38572
|
+
emit(args.json, checks, () => formatDoctor(checks, paint, help));
|
|
37365
38573
|
if (checks.some((check3) => check3.status === "FAIL")) process.exitCode = 1;
|
|
37366
38574
|
})
|
|
37367
38575
|
});
|
|
@@ -37374,7 +38582,7 @@ var projects = defineCommand({
|
|
|
37374
38582
|
run: ({ args }) => action(async () => {
|
|
37375
38583
|
const client = await clientFor({ host: args.host });
|
|
37376
38584
|
const list = await client.listProjects();
|
|
37377
|
-
emit(args.json, list, () => formatProjects(list, paint));
|
|
38585
|
+
emit(args.json, list, () => formatProjects(list, paint, width()));
|
|
37378
38586
|
})
|
|
37379
38587
|
});
|
|
37380
38588
|
var lockfileArg = {
|
|
@@ -37487,7 +38695,11 @@ var status = defineCommand({
|
|
|
37487
38695
|
const client = await clientFor({ host: args.host });
|
|
37488
38696
|
if (!args.project) {
|
|
37489
38697
|
const list = await client.listProjects();
|
|
37490
|
-
return emit(
|
|
38698
|
+
return emit(
|
|
38699
|
+
args.json,
|
|
38700
|
+
list,
|
|
38701
|
+
() => formatProjects(list, paint, width())
|
|
38702
|
+
);
|
|
37491
38703
|
}
|
|
37492
38704
|
const project = await findProject(client, args.project);
|
|
37493
38705
|
const report = await client.projectStatus(project.id);
|
|
@@ -37526,9 +38738,9 @@ var runs = defineCommand({
|
|
|
37526
38738
|
"status",
|
|
37527
38739
|
CLI_RUN_STATUSES
|
|
37528
38740
|
),
|
|
37529
|
-
limit:
|
|
38741
|
+
limit: limitArg(args.limit)
|
|
37530
38742
|
});
|
|
37531
|
-
emit(args.json, list, () => formatRuns(list, paint));
|
|
38743
|
+
emit(args.json, list, () => formatRuns(list, paint, width()));
|
|
37532
38744
|
})
|
|
37533
38745
|
});
|
|
37534
38746
|
var run = defineCommand({
|
|
@@ -37539,7 +38751,7 @@ var run = defineCommand({
|
|
|
37539
38751
|
},
|
|
37540
38752
|
run: ({ args }) => action(async () => {
|
|
37541
38753
|
const client = await clientFor({ host: args.host });
|
|
37542
|
-
const detail = await client.getRun(args.runId);
|
|
38754
|
+
const detail = await client.getRun(idArg(args.runId, "Run"));
|
|
37543
38755
|
emit(
|
|
37544
38756
|
args.json,
|
|
37545
38757
|
detail,
|
|
@@ -37548,18 +38760,19 @@ var run = defineCommand({
|
|
|
37548
38760
|
})
|
|
37549
38761
|
});
|
|
37550
38762
|
async function follow(client, runId, args) {
|
|
37551
|
-
|
|
37552
|
-
const
|
|
37553
|
-
|
|
37554
|
-
|
|
37555
|
-
|
|
37556
|
-
|
|
37557
|
-
|
|
37558
|
-
|
|
37559
|
-
|
|
37560
|
-
|
|
37561
|
-
|
|
37562
|
-
|
|
38763
|
+
const threshold = failOnArg(args.failOn);
|
|
38764
|
+
const live = progress(epaint, { quiet: args.json });
|
|
38765
|
+
let run2;
|
|
38766
|
+
try {
|
|
38767
|
+
run2 = await waitForRun(client, runId, {
|
|
38768
|
+
intervalMs: pollSeconds(args.interval) * 1e3,
|
|
38769
|
+
timeoutMs: waitSeconds(args.timeout, 600) * 1e3,
|
|
38770
|
+
sleep,
|
|
38771
|
+
onUpdate: (update) => live.update(progressLine(update, false), update.progress?.percent)
|
|
38772
|
+
});
|
|
38773
|
+
} finally {
|
|
38774
|
+
live.stop();
|
|
38775
|
+
}
|
|
37563
38776
|
emit(
|
|
37564
38777
|
args.json,
|
|
37565
38778
|
run2,
|
|
@@ -37570,6 +38783,7 @@ async function follow(client, runId, args) {
|
|
|
37570
38783
|
`Wait ended; ${run2.id} is still ${run2.status}. Resume with lockpick watch ${run2.id}.`
|
|
37571
38784
|
);
|
|
37572
38785
|
if (run2.status !== "succeeded") process.exitCode = EXIT.unfinished;
|
|
38786
|
+
else gate(run2.open, threshold, args.json);
|
|
37573
38787
|
}
|
|
37574
38788
|
var watch = defineCommand({
|
|
37575
38789
|
meta: {
|
|
@@ -37578,6 +38792,7 @@ var watch = defineCommand({
|
|
|
37578
38792
|
},
|
|
37579
38793
|
args: {
|
|
37580
38794
|
runId: { type: "positional", description: "Run id", required: true },
|
|
38795
|
+
...failOnFlag,
|
|
37581
38796
|
interval: {
|
|
37582
38797
|
type: "string",
|
|
37583
38798
|
description: "Seconds between checks (default 5, backs off to 30)"
|
|
@@ -37589,8 +38804,9 @@ var watch = defineCommand({
|
|
|
37589
38804
|
...common
|
|
37590
38805
|
},
|
|
37591
38806
|
run: ({ args }) => action(async () => {
|
|
38807
|
+
failOnArg(args.failOn);
|
|
37592
38808
|
const client = await clientFor({ host: args.host });
|
|
37593
|
-
await follow(client, args.runId, args);
|
|
38809
|
+
await follow(client, idArg(args.runId, "Run"), args);
|
|
37594
38810
|
})
|
|
37595
38811
|
});
|
|
37596
38812
|
var pulse = defineCommand({
|
|
@@ -37617,12 +38833,14 @@ var pulse = defineCommand({
|
|
|
37617
38833
|
type: "string",
|
|
37618
38834
|
description: "Maximum wait in seconds (default 600, max 3600)"
|
|
37619
38835
|
},
|
|
38836
|
+
...failOnFlag,
|
|
37620
38837
|
...lockfileArg,
|
|
37621
38838
|
...common
|
|
37622
38839
|
},
|
|
37623
38840
|
run: ({ args }) => action(async () => {
|
|
37624
38841
|
pollSeconds(args.interval);
|
|
37625
38842
|
waitSeconds(args.timeout, 600);
|
|
38843
|
+
failOnArg(args.failOn);
|
|
37626
38844
|
const client = await clientFor({ host: args.host });
|
|
37627
38845
|
const project = await findProject(client, args.project);
|
|
37628
38846
|
const run2 = await client.startPulse(project.id, randomUUID6());
|
|
@@ -37635,7 +38853,7 @@ var pulse = defineCommand({
|
|
|
37635
38853
|
);
|
|
37636
38854
|
if (!args.json)
|
|
37637
38855
|
err(
|
|
37638
|
-
|
|
38856
|
+
`${epaint("green", "\u2714")} Started Pulse ${run2.id} on ${run2.target}${run2.baselineRunId ? ` (recheck of ${run2.baselineRunId})` : ""}.`
|
|
37639
38857
|
);
|
|
37640
38858
|
await follow(client, run2.id, args);
|
|
37641
38859
|
})
|
|
@@ -37670,15 +38888,21 @@ var check2 = defineCommand({
|
|
|
37670
38888
|
idempotencyKey: randomUUID6()
|
|
37671
38889
|
});
|
|
37672
38890
|
const deadline = Date.now() + timeout * 1e3;
|
|
37673
|
-
|
|
37674
|
-
|
|
37675
|
-
|
|
37676
|
-
|
|
37677
|
-
|
|
37678
|
-
|
|
37679
|
-
|
|
37680
|
-
|
|
37681
|
-
|
|
38891
|
+
const live = progress(epaint, { quiet: args.json || !args.wait });
|
|
38892
|
+
try {
|
|
38893
|
+
while (args.wait && (result.status === "queued" || result.status === "running") && Date.now() < deadline) {
|
|
38894
|
+
live.update(`Quick check of ${args.url} \xB7 ${result.status}`);
|
|
38895
|
+
await sleep(
|
|
38896
|
+
Math.min(
|
|
38897
|
+
(result.suggestedPollAfterSeconds ?? 5) * 1e3,
|
|
38898
|
+
deadline - Date.now()
|
|
38899
|
+
)
|
|
38900
|
+
);
|
|
38901
|
+
if (Date.now() >= deadline) break;
|
|
38902
|
+
result = await client.agent.getQuickCheck(result.checkId);
|
|
38903
|
+
}
|
|
38904
|
+
} finally {
|
|
38905
|
+
live.stop();
|
|
37682
38906
|
}
|
|
37683
38907
|
emit(args.json, result, () => formatQuickCheck(result, paint));
|
|
37684
38908
|
if (args.wait && (result.status === "queued" || result.status === "running"))
|
|
@@ -37737,8 +38961,10 @@ var sitesSwitch = defineCommand({
|
|
|
37737
38961
|
},
|
|
37738
38962
|
run: ({ args }) => action(async () => {
|
|
37739
38963
|
if (!args.yes)
|
|
37740
|
-
throw new
|
|
37741
|
-
`This removes site ${args.site} from monitoring. Run it again with --yes to confirm
|
|
38964
|
+
throw new LockpickApiError(
|
|
38965
|
+
`This removes site ${args.site} from monitoring. Run it again with --yes to confirm.`,
|
|
38966
|
+
400,
|
|
38967
|
+
"VALIDATION_ERROR"
|
|
37742
38968
|
);
|
|
37743
38969
|
const client = await clientFor({ host: args.host });
|
|
37744
38970
|
const switched = await client.agent.switchFreeSite({
|
|
@@ -37762,7 +38988,7 @@ var sites = defineCommand({
|
|
|
37762
38988
|
return action(async () => {
|
|
37763
38989
|
const client = await clientFor({ host: args.host });
|
|
37764
38990
|
const list = await client.listProjects();
|
|
37765
|
-
emit(args.json, list, () => formatProjects(list, paint));
|
|
38991
|
+
emit(args.json, list, () => formatProjects(list, paint, width()));
|
|
37766
38992
|
});
|
|
37767
38993
|
}
|
|
37768
38994
|
});
|
|
@@ -37794,23 +39020,33 @@ var recheck = defineCommand({
|
|
|
37794
39020
|
type: "string",
|
|
37795
39021
|
description: "Maximum wait in seconds (default 600, max 3600)"
|
|
37796
39022
|
},
|
|
39023
|
+
...failOnFlag,
|
|
37797
39024
|
...lockfileArg,
|
|
37798
39025
|
...common
|
|
37799
39026
|
},
|
|
37800
39027
|
run: ({ args }) => action(async () => {
|
|
37801
39028
|
pollSeconds(args.interval);
|
|
37802
39029
|
waitSeconds(args.timeout, 600);
|
|
39030
|
+
failOnArg(args.failOn);
|
|
37803
39031
|
const client = await clientFor({ host: args.host });
|
|
37804
39032
|
if (args.dns) {
|
|
39033
|
+
idArg(args.target, "Scan");
|
|
39034
|
+
idArg(args.dns, "Finding");
|
|
37805
39035
|
let result = await client.agent.recheckDns(args.target, {
|
|
37806
39036
|
findingId: args.dns,
|
|
37807
39037
|
idempotencyKey: randomUUID6()
|
|
37808
39038
|
});
|
|
37809
39039
|
const deadline = Date.now() + waitSeconds(args.timeout, 60) * 1e3;
|
|
37810
|
-
|
|
37811
|
-
|
|
37812
|
-
|
|
37813
|
-
|
|
39040
|
+
const live = progress(epaint, { quiet: args.json || !args.wait });
|
|
39041
|
+
try {
|
|
39042
|
+
while (args.wait && result.pending && Date.now() < deadline) {
|
|
39043
|
+
live.update(`Reading DNS records again for ${args.dns}`);
|
|
39044
|
+
await sleep(Math.min(3e3, deadline - Date.now()));
|
|
39045
|
+
if (Date.now() >= deadline) break;
|
|
39046
|
+
result = await client.agent.getDnsRecheck(args.target);
|
|
39047
|
+
}
|
|
39048
|
+
} finally {
|
|
39049
|
+
live.stop();
|
|
37814
39050
|
}
|
|
37815
39051
|
emit(args.json, result, () => formatDnsRecheck(result, paint));
|
|
37816
39052
|
if (args.wait && result.pending) {
|
|
@@ -37908,7 +39144,7 @@ var verifyCommand = (name, description) => defineCommand({
|
|
|
37908
39144
|
"Not seen. Once it is published, run lockpick verify again. The value stays the same."
|
|
37909
39145
|
)
|
|
37910
39146
|
);
|
|
37911
|
-
process.exitCode =
|
|
39147
|
+
process.exitCode = EXIT.unfinished;
|
|
37912
39148
|
})
|
|
37913
39149
|
});
|
|
37914
39150
|
var deepVerify = verifyCommand(
|
|
@@ -37942,14 +39178,14 @@ var deepCheck = defineCommand({
|
|
|
37942
39178
|
const project = await findProject(client, args.site);
|
|
37943
39179
|
const verification = await client.agent.checkDeepVerification(
|
|
37944
39180
|
project.id,
|
|
37945
|
-
args.verificationId
|
|
39181
|
+
idArg(args.verificationId, "Proof")
|
|
37946
39182
|
);
|
|
37947
39183
|
emit(
|
|
37948
39184
|
args.json,
|
|
37949
39185
|
verification,
|
|
37950
39186
|
() => formatVerification(verification, paint)
|
|
37951
39187
|
);
|
|
37952
|
-
if (verification.state !== "verified") process.exitCode =
|
|
39188
|
+
if (verification.state !== "verified") process.exitCode = EXIT.unfinished;
|
|
37953
39189
|
})
|
|
37954
39190
|
});
|
|
37955
39191
|
var deepPrepare = defineCommand({
|
|
@@ -37979,7 +39215,7 @@ var deepPrepare = defineCommand({
|
|
|
37979
39215
|
const client = await clientFor({ host: args.host });
|
|
37980
39216
|
const project = await findProject(client, args.site);
|
|
37981
39217
|
const prepared = await client.agent.prepareDeep(project.id, {
|
|
37982
|
-
verificationId: args.verificationId,
|
|
39218
|
+
verificationId: idArg(args.verificationId, "Proof"),
|
|
37983
39219
|
idempotencyKey: randomUUID6()
|
|
37984
39220
|
});
|
|
37985
39221
|
emit(args.json, prepared, () => formatPreparedDeep(prepared, paint));
|
|
@@ -38011,13 +39247,17 @@ var deepStart = defineCommand({
|
|
|
38011
39247
|
type: "string",
|
|
38012
39248
|
description: "Maximum wait in seconds (default 600, max 3600)"
|
|
38013
39249
|
},
|
|
39250
|
+
...failOnFlag,
|
|
38014
39251
|
...common
|
|
38015
39252
|
},
|
|
38016
39253
|
run: ({ args }) => action(async () => {
|
|
38017
39254
|
pollSeconds(args.interval);
|
|
38018
39255
|
waitSeconds(args.timeout, 600);
|
|
39256
|
+
failOnArg(args.failOn);
|
|
38019
39257
|
const client = await clientFor({ host: args.host });
|
|
38020
|
-
const launch = await client.agent.startDeep(
|
|
39258
|
+
const launch = await client.agent.startDeep(
|
|
39259
|
+
idArg(args.operationId, "Operation")
|
|
39260
|
+
);
|
|
38021
39261
|
const started = `Started Deep scan ${launch.scan.id} on ${launch.scan.target}. ${chargeText(launch)}`;
|
|
38022
39262
|
if (!args.wait)
|
|
38023
39263
|
return emit(
|
|
@@ -38055,11 +39295,13 @@ var findings = defineCommand({
|
|
|
38055
39295
|
description: "Include passing and not-assessed checks",
|
|
38056
39296
|
default: false
|
|
38057
39297
|
},
|
|
39298
|
+
...failOnFlag,
|
|
38058
39299
|
...common
|
|
38059
39300
|
},
|
|
38060
39301
|
run: ({ args }) => action(async () => {
|
|
39302
|
+
const threshold = failOnArg(args.failOn);
|
|
38061
39303
|
const client = await clientFor({ host: args.host });
|
|
38062
|
-
const list = await client.listFindings(args.runId, {
|
|
39304
|
+
const list = await client.listFindings(idArg(args.runId, "Run"), {
|
|
38063
39305
|
severity: parse3(
|
|
38064
39306
|
cliSeveritySchema,
|
|
38065
39307
|
args.severity,
|
|
@@ -38068,7 +39310,12 @@ var findings = defineCommand({
|
|
|
38068
39310
|
),
|
|
38069
39311
|
all: args.all
|
|
38070
39312
|
});
|
|
38071
|
-
emit(args.json, list, () => formatFindings(list, paint));
|
|
39313
|
+
emit(args.json, list, () => formatFindings(list, paint, width()));
|
|
39314
|
+
gate(
|
|
39315
|
+
openCounts(list.filter((item) => item.state === "attention")),
|
|
39316
|
+
threshold,
|
|
39317
|
+
args.json
|
|
39318
|
+
);
|
|
38072
39319
|
})
|
|
38073
39320
|
});
|
|
38074
39321
|
var finding = defineCommand({
|
|
@@ -38084,7 +39331,10 @@ var finding = defineCommand({
|
|
|
38084
39331
|
},
|
|
38085
39332
|
run: ({ args }) => action(async () => {
|
|
38086
39333
|
const client = await clientFor({ host: args.host });
|
|
38087
|
-
const detail = await client.getFinding(
|
|
39334
|
+
const detail = await client.getFinding(
|
|
39335
|
+
idArg(args.runId, "Run"),
|
|
39336
|
+
idArg(args.findingId, "Finding")
|
|
39337
|
+
);
|
|
38088
39338
|
emit(
|
|
38089
39339
|
args.json,
|
|
38090
39340
|
detail,
|
|
@@ -38107,7 +39357,11 @@ var brief = defineCommand({
|
|
|
38107
39357
|
run: ({ args }) => action(async () => {
|
|
38108
39358
|
const agent2 = agentArg(args.agent);
|
|
38109
39359
|
const client = await clientFor({ host: args.host });
|
|
38110
|
-
const result = await client.repairBrief(
|
|
39360
|
+
const result = await client.repairBrief(
|
|
39361
|
+
idArg(args.runId, "Run"),
|
|
39362
|
+
args.finding === void 0 ? void 0 : idArg(args.finding, "Finding"),
|
|
39363
|
+
agent2
|
|
39364
|
+
);
|
|
38111
39365
|
if (args.out) {
|
|
38112
39366
|
await writeFile2(args.out, `${result.markdown}
|
|
38113
39367
|
`);
|
|
@@ -38139,7 +39393,11 @@ var fix = defineCommand({
|
|
|
38139
39393
|
run: ({ args }) => action(async () => {
|
|
38140
39394
|
const agent2 = agentArg(args.agent);
|
|
38141
39395
|
const client = await clientFor({ host: args.host });
|
|
38142
|
-
const result = await client.repairBrief(
|
|
39396
|
+
const result = await client.repairBrief(
|
|
39397
|
+
idArg(args.runId, "Run"),
|
|
39398
|
+
void 0,
|
|
39399
|
+
agent2
|
|
39400
|
+
);
|
|
38143
39401
|
if (result.findingIds.length === 0) {
|
|
38144
39402
|
if (args.json)
|
|
38145
39403
|
return emit(true, { ...result, file: null, command: null }, () => "");
|
|
@@ -38178,7 +39436,7 @@ var compare = defineCommand({
|
|
|
38178
39436
|
},
|
|
38179
39437
|
run: ({ args }) => action(async () => {
|
|
38180
39438
|
const client = await clientFor({ host: args.host });
|
|
38181
|
-
const comparison = await client.comparison(args.runId);
|
|
39439
|
+
const comparison = await client.comparison(idArg(args.runId, "Run"));
|
|
38182
39440
|
emit(args.json, comparison, () => formatComparison(comparison, paint));
|
|
38183
39441
|
})
|
|
38184
39442
|
});
|
|
@@ -38411,6 +39669,159 @@ var skills = defineCommand({
|
|
|
38411
39669
|
);
|
|
38412
39670
|
})
|
|
38413
39671
|
});
|
|
39672
|
+
var docs = defineCommand({
|
|
39673
|
+
meta: {
|
|
39674
|
+
name: "docs",
|
|
39675
|
+
description: "Search the Lockpick docs from the terminal, or list the CLI guides (--open opens the first match)"
|
|
39676
|
+
},
|
|
39677
|
+
args: {
|
|
39678
|
+
query: {
|
|
39679
|
+
type: "positional",
|
|
39680
|
+
description: 'What to look for, e.g. "fix a missing CSP"',
|
|
39681
|
+
required: false
|
|
39682
|
+
},
|
|
39683
|
+
open: {
|
|
39684
|
+
type: "boolean",
|
|
39685
|
+
description: "Open the best match (or the CLI guide) in the browser",
|
|
39686
|
+
default: false
|
|
39687
|
+
},
|
|
39688
|
+
...common
|
|
39689
|
+
},
|
|
39690
|
+
run: ({ args, rawArgs }) => action(async () => {
|
|
39691
|
+
const hostValue = rawArgs.indexOf("--host") + 1;
|
|
39692
|
+
const query = rawArgs.filter(
|
|
39693
|
+
(arg, index) => !arg.startsWith("-") && (hostValue === 0 || index !== hostValue)
|
|
39694
|
+
).join(" ").trim();
|
|
39695
|
+
if (!query) {
|
|
39696
|
+
const session = await resolveSession({ host: args.host });
|
|
39697
|
+
const pages = [
|
|
39698
|
+
["Install and sign in", "cli"],
|
|
39699
|
+
["Every command", "commands"],
|
|
39700
|
+
["Keys and environment", "configuration"],
|
|
39701
|
+
["Run Lockpick in CI", "ci"],
|
|
39702
|
+
["What each permission allows", "permissions"],
|
|
39703
|
+
["Pulse or Deep", "pulseOrDeep"],
|
|
39704
|
+
["Fix what Lockpick found", "fix"]
|
|
39705
|
+
].map(([title, page]) => ({
|
|
39706
|
+
title,
|
|
39707
|
+
url: docsUrl(session.host, page)
|
|
39708
|
+
}));
|
|
39709
|
+
emit(
|
|
39710
|
+
args.json,
|
|
39711
|
+
pages,
|
|
39712
|
+
() => [
|
|
39713
|
+
...pages.map(
|
|
39714
|
+
(page) => `${page.title.padEnd(28)} ${paint("cyan", page.url)}`
|
|
39715
|
+
),
|
|
39716
|
+
"",
|
|
39717
|
+
paint("dim", "Search them with: lockpick docs <words>")
|
|
39718
|
+
].join("\n")
|
|
39719
|
+
);
|
|
39720
|
+
if (args.open && !args.json) await openBrowser(pages[0].url);
|
|
39721
|
+
return;
|
|
39722
|
+
}
|
|
39723
|
+
const client = await clientFor({ host: args.host });
|
|
39724
|
+
const found = await client.agent.searchDocs({ query, limit: 5 });
|
|
39725
|
+
emit(args.json, found, () => formatDocSearch(found, paint));
|
|
39726
|
+
if (args.open && !args.json && found.results[0])
|
|
39727
|
+
await openBrowser(found.results[0].url);
|
|
39728
|
+
})
|
|
39729
|
+
});
|
|
39730
|
+
var ciInit = defineCommand({
|
|
39731
|
+
meta: {
|
|
39732
|
+
name: "init",
|
|
39733
|
+
description: `Write a GitHub Actions workflow (${CI_WORKFLOW_FILE}) that runs a Pulse on each release and fails on serious findings`
|
|
39734
|
+
},
|
|
39735
|
+
args: {
|
|
39736
|
+
site: {
|
|
39737
|
+
type: "positional",
|
|
39738
|
+
description: "Site id or name to check",
|
|
39739
|
+
required: true
|
|
39740
|
+
},
|
|
39741
|
+
failOn: {
|
|
39742
|
+
type: "string",
|
|
39743
|
+
description: `Fail the workflow at this severity or worse: ${FAIL_ON.join(", ")} (default high)`,
|
|
39744
|
+
default: "high"
|
|
39745
|
+
},
|
|
39746
|
+
file: {
|
|
39747
|
+
type: "string",
|
|
39748
|
+
description: `Where to write it (default ${CI_WORKFLOW_FILE})`
|
|
39749
|
+
},
|
|
39750
|
+
action: {
|
|
39751
|
+
type: "boolean",
|
|
39752
|
+
description: "Use the Lockpick GitHub Action instead of running the CLI with npx (preview)",
|
|
39753
|
+
default: false
|
|
39754
|
+
},
|
|
39755
|
+
force: {
|
|
39756
|
+
type: "boolean",
|
|
39757
|
+
description: "Replace an existing workflow file",
|
|
39758
|
+
default: false
|
|
39759
|
+
},
|
|
39760
|
+
dryRun: {
|
|
39761
|
+
type: "boolean",
|
|
39762
|
+
description: "Print the workflow instead of writing it",
|
|
39763
|
+
default: false
|
|
39764
|
+
},
|
|
39765
|
+
...common
|
|
39766
|
+
},
|
|
39767
|
+
run: ({ args }) => action(async () => {
|
|
39768
|
+
const failOn = failOnArg(args.failOn) ?? "high";
|
|
39769
|
+
const client = await clientFor({ host: args.host });
|
|
39770
|
+
const project = await findProject(client, args.site);
|
|
39771
|
+
const workflow = githubWorkflow({
|
|
39772
|
+
host: client.host,
|
|
39773
|
+
siteId: project.id,
|
|
39774
|
+
siteName: project.name,
|
|
39775
|
+
failOn,
|
|
39776
|
+
cliVersion: CLI_VERSION,
|
|
39777
|
+
docsUrl: docsUrl(client.host, "ci"),
|
|
39778
|
+
action: args.action,
|
|
39779
|
+
actionRef: process.env.LOCKPICK_ACTION_REF
|
|
39780
|
+
});
|
|
39781
|
+
const file2 = args.file ?? CI_WORKFLOW_FILE;
|
|
39782
|
+
const warnings = isPrivateHost(client.host) ? [
|
|
39783
|
+
`${client.host} is not reachable from GitHub-hosted runners. Use your public Lockpick address with --host, or a self-hosted runner.`
|
|
39784
|
+
] : [];
|
|
39785
|
+
if (args.dryRun)
|
|
39786
|
+
return emit(args.json, { file: file2, workflow, warnings }, () => workflow);
|
|
39787
|
+
if (!args.force && await access2(file2).then(
|
|
39788
|
+
() => true,
|
|
39789
|
+
() => false
|
|
39790
|
+
))
|
|
39791
|
+
throw new LockpickApiError(
|
|
39792
|
+
`${file2} already exists. Pass --force to replace it, or --dry-run to see the new one.`,
|
|
39793
|
+
400,
|
|
39794
|
+
"VALIDATION_ERROR"
|
|
39795
|
+
);
|
|
39796
|
+
await mkdir4(dirname5(file2), { recursive: true });
|
|
39797
|
+
await writeFile2(file2, workflow);
|
|
39798
|
+
emit(
|
|
39799
|
+
args.json,
|
|
39800
|
+
{ file: file2, workflow, warnings },
|
|
39801
|
+
() => [
|
|
39802
|
+
`${paint("green", "\u2714")} Wrote ${file2} for ${project.name} (${project.id}).`,
|
|
39803
|
+
...warnings.map((warning) => paint("yellow", `! ${warning}`)),
|
|
39804
|
+
"",
|
|
39805
|
+
"Next:",
|
|
39806
|
+
` 1. Save a Lockpick key allowed to start Pulses as the repository secret ${CI_KEY_SECRET}:`,
|
|
39807
|
+
` gh secret set ${CI_KEY_SECRET}`,
|
|
39808
|
+
` 2. Commit ${file2}, then run it from the Actions tab or publish a release.`,
|
|
39809
|
+
paint(
|
|
39810
|
+
"dim",
|
|
39811
|
+
`Each run uses one Pulse (or 1 credit). It fails on ${failOn} findings or worse.`
|
|
39812
|
+
),
|
|
39813
|
+
...docsUrl(client.host, "ci") ? [paint("dim", `Docs: ${docsUrl(client.host, "ci")}`)] : []
|
|
39814
|
+
].join("\n")
|
|
39815
|
+
);
|
|
39816
|
+
})
|
|
39817
|
+
});
|
|
39818
|
+
var ci = defineCommand({
|
|
39819
|
+
meta: {
|
|
39820
|
+
name: "ci",
|
|
39821
|
+
description: "Run Lockpick in CI: `lockpick ci init <site>` writes a GitHub Actions workflow"
|
|
39822
|
+
},
|
|
39823
|
+
subCommands: { init: ciInit }
|
|
39824
|
+
});
|
|
38414
39825
|
var main = defineCommand({
|
|
38415
39826
|
meta: {
|
|
38416
39827
|
name: "lockpick",
|
|
@@ -38442,7 +39853,9 @@ var main = defineCommand({
|
|
|
38442
39853
|
import: importFindings,
|
|
38443
39854
|
open: open3,
|
|
38444
39855
|
mcp,
|
|
38445
|
-
skills
|
|
39856
|
+
skills,
|
|
39857
|
+
docs,
|
|
39858
|
+
ci
|
|
38446
39859
|
}
|
|
38447
39860
|
});
|
|
38448
39861
|
|