@bbradar/mcp 0.1.19 → 0.2.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 +74 -2
- package/benchmark/mcp-benchmark.mjs +167 -0
- package/dist/bbradarClient.d.ts +158 -3
- package/dist/bbradarClient.js +797 -57
- package/dist/bbradarClient.js.map +1 -1
- package/dist/cache.d.ts +13 -2
- package/dist/cache.js +36 -7
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +53 -6
- package/dist/config.js.map +1 -1
- package/dist/cursorStore.d.ts +33 -0
- package/dist/cursorStore.js +123 -0
- package/dist/cursorStore.js.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +164 -7
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +1119 -161
- package/dist/server.js.map +1 -1
- package/package.json +4 -2
package/dist/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { BBRadarApiError } from "./bbradarClient.js";
|
|
5
|
+
import { NumericCursorStore } from "./cursorStore.js";
|
|
5
6
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
6
7
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
7
|
-
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
8
|
-
const SERVER_VERSION = "0.
|
|
8
|
+
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
+
const SERVER_VERSION = "0.2.0";
|
|
9
10
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
10
11
|
const MAX_TARGET_EXPORT = 500;
|
|
11
12
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -20,7 +21,7 @@ const MAX_FIND_TARGET_SAMPLE_PROGRAMS = 10;
|
|
|
20
21
|
const MAX_FIND_TARGET_SAMPLES = 20;
|
|
21
22
|
const MAX_FIND_UPSTREAM_REQUEST_BUDGET = 50;
|
|
22
23
|
const DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET = 20;
|
|
23
|
-
const DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET =
|
|
24
|
+
const DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET = 12;
|
|
24
25
|
const MAX_ACCEPTED_NUMERIC_LIMIT = 1_000;
|
|
25
26
|
const PROGRAM_COLLECTION_CONCURRENCY = 4;
|
|
26
27
|
const TARGET_SAMPLE_CONCURRENCY = 3;
|
|
@@ -29,13 +30,40 @@ const MAX_FILTER_DISCOVERY_PAGES = 5;
|
|
|
29
30
|
const DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS = 10;
|
|
30
31
|
const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
|
|
31
32
|
const MAX_LOCAL_EXPORT_RESOURCES = 25;
|
|
33
|
+
const MAX_LOCAL_EXPORT_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
const LOCAL_EXPORT_TTL_MS = 10 * 60 * 1000;
|
|
32
35
|
const EXPORT_PREVIEW_LIMIT = 25;
|
|
33
36
|
const STALE_PROGRAM_ID_RESOLVE_PAGES = 1;
|
|
34
|
-
const STALE_PROGRAM_ID_RESOLVE_BUDGET =
|
|
37
|
+
const STALE_PROGRAM_ID_RESOLVE_BUDGET = 12;
|
|
35
38
|
const PROGRAM_FALLBACK_CACHE_MAX_ENTRIES = 200;
|
|
39
|
+
const MAX_IN_MEMORY_FEED_CURSORS = 500;
|
|
36
40
|
const MAX_RESOLVE_SEARCH_QUERIES = 4;
|
|
41
|
+
const FAST_RESOLVE_SEARCH_QUERIES = 2;
|
|
37
42
|
const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
|
|
38
43
|
const SDK_VERSION = "1.29.0";
|
|
44
|
+
const CORE_TOOL_NAMES = new Set([
|
|
45
|
+
"search_programs",
|
|
46
|
+
"get_program",
|
|
47
|
+
"resolve_program",
|
|
48
|
+
"run_program_name_action",
|
|
49
|
+
"get_program_eligible_targets_by_name",
|
|
50
|
+
"get_program_targets",
|
|
51
|
+
"get_recent_changes",
|
|
52
|
+
"get_repository_changes",
|
|
53
|
+
"get_latest_added_targets",
|
|
54
|
+
"get_opportunities",
|
|
55
|
+
"find_programs",
|
|
56
|
+
"find_programs_by_target",
|
|
57
|
+
"list_filters",
|
|
58
|
+
"get_bbradar_guide",
|
|
59
|
+
"get_mcp_status",
|
|
60
|
+
"export_targets"
|
|
61
|
+
]);
|
|
62
|
+
const TOOL_ENDPOINT_REQUIREMENTS = {
|
|
63
|
+
get_repository_changes: "repository_changes",
|
|
64
|
+
export_targets: "target_export",
|
|
65
|
+
get_opportunities: "opportunities"
|
|
66
|
+
};
|
|
39
67
|
const PROGRAM_ID_PLATFORM_GUESSES = [
|
|
40
68
|
"HackerOne",
|
|
41
69
|
"BugCrowd",
|
|
@@ -141,6 +169,7 @@ const TARGET_QUERY_STOP_WORDS = new Set([
|
|
|
141
169
|
]);
|
|
142
170
|
const targetListModeSchema = z.enum(["identifiers", "compact", "full"]);
|
|
143
171
|
const changeListModeSchema = z.enum(["compact", "full"]);
|
|
172
|
+
const repositoryChangeListModeSchema = z.enum(["compact", "full"]);
|
|
144
173
|
const programListModeSchema = z.enum(["compact", "full"]);
|
|
145
174
|
const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
|
|
146
175
|
const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
|
|
@@ -209,6 +238,8 @@ const pageSchema = z.number().int().min(1).default(1);
|
|
|
209
238
|
const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
|
|
210
239
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
|
|
211
240
|
const opportunityLevelSchema = z.enum(["elite", "hot", "strong", "potential"]);
|
|
241
|
+
const targetOpportunityLabelSchema = z.enum(["Elite Target", "Strong Target", "Potential Target", "Normal"]);
|
|
242
|
+
const competitionRiskSchema = z.enum(["Low", "Medium", "High", "Unknown"]);
|
|
212
243
|
const programSortSchema = z
|
|
213
244
|
.enum([
|
|
214
245
|
"best_match",
|
|
@@ -260,6 +291,7 @@ function toolTimingOutputSchema() {
|
|
|
260
291
|
function cacheOutputSchema() {
|
|
261
292
|
return z.object({
|
|
262
293
|
hit: z.boolean(),
|
|
294
|
+
stale_while_revalidate: z.boolean().optional(),
|
|
263
295
|
coalesced_live_request: z.boolean().optional(),
|
|
264
296
|
expires_at: z.string().optional()
|
|
265
297
|
});
|
|
@@ -346,12 +378,28 @@ const programCandidatePresetInputSchema = {
|
|
|
346
378
|
output_mode: programListModeSchema.default("compact"),
|
|
347
379
|
upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
|
|
348
380
|
};
|
|
381
|
+
function createProfiledToolRegistrar(server, profile, client) {
|
|
382
|
+
return (name, toolConfig, callback) => {
|
|
383
|
+
const registeredTool = server.registerTool(name, toolConfig, callback);
|
|
384
|
+
const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
|
|
385
|
+
if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
|
|
386
|
+
(requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
|
|
387
|
+
registeredTool.disable();
|
|
388
|
+
}
|
|
389
|
+
return registeredTool;
|
|
390
|
+
};
|
|
391
|
+
}
|
|
349
392
|
export function createBbradarServer(client, config) {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
393
|
+
const instructions = config.toolProfile === "core"
|
|
394
|
+
? [
|
|
395
|
+
"Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
|
|
396
|
+
"This server uses the compact core tool profile. Start with get_bbradar_guide when unsure.",
|
|
397
|
+
"Use resolve_program or run_program_name_action for a named program.",
|
|
398
|
+
"Use find_programs for general discovery and find_programs_by_target for reverse target lookup.",
|
|
399
|
+
"Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
|
|
400
|
+
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
401
|
+
]
|
|
402
|
+
: [
|
|
355
403
|
"Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
|
|
356
404
|
"When unsure which BBRadar tool to use, call get_bbradar_guide first. It is local-only and cheap.",
|
|
357
405
|
"Simple default path: find_program_candidates for discovery, run_program_name_action for one named program, get_program_eligible_targets_by_name for target lists by program name.",
|
|
@@ -359,18 +407,39 @@ export function createBbradarServer(client, config) {
|
|
|
359
407
|
"Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
|
|
360
408
|
"Default to bounty-eligible in-scope data. Include out-of-scope, ineligible, VDP, disclosure-only, or no-bounty data only when the user explicitly asks.",
|
|
361
409
|
"Recently updated programs across platforms: find_recently_updated_programs. Do not infer a platform such as Immunefi unless the user explicitly names it.",
|
|
410
|
+
"Self-Hosted programs use Self-Hosted:slug ids and are available through program search, exact detail, and target tools. Respect data_capabilities; do not infer missing opportunity, report-count, target-change, submission-cost, or repository signals.",
|
|
362
411
|
"Freshness: find_recently_updated_programs, get_latest_added_targets, check_program_new_targets, check_watchlist_new_targets, find_recent_by_type, get_program_scope_delta.",
|
|
363
|
-
"
|
|
412
|
+
"Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
|
|
413
|
+
"Discovery: find_program_candidates, find_wildcard_programs, find_recently_added_wildcards, find_low_competition_high_reward, find_stack_matches, find_language_programs, find_target_type_programs, find_reward_programs, find_paid_programs, find_vdp_programs, find_web3_contests, find_low_noise_programs.",
|
|
364
414
|
"Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
|
|
365
415
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
366
|
-
]
|
|
416
|
+
];
|
|
417
|
+
const server = new McpServer({
|
|
418
|
+
name: "bbradar.io",
|
|
419
|
+
version: SERVER_VERSION
|
|
420
|
+
}, {
|
|
421
|
+
instructions: instructions.join("\n")
|
|
367
422
|
});
|
|
423
|
+
const registerTool = createProfiledToolRegistrar(server, config.toolProfile, client);
|
|
368
424
|
const rateLimiter = new FixedWindowRateLimiter();
|
|
369
425
|
const exportStore = new Map();
|
|
370
|
-
|
|
426
|
+
const continuationStore = new Map();
|
|
427
|
+
const feedCursorStore = new NumericCursorStore(config.cursorStatePath, MAX_IN_MEMORY_FEED_CURSORS);
|
|
428
|
+
const maxProgramsPageSize = client.endpointNumber("programs", "max_page_size", MAX_PROGRAMS_PER_SEARCH);
|
|
429
|
+
const maxProgramTargetsPageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
430
|
+
const maxTargetChangesPageSize = client.endpointNumber("target_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
431
|
+
const maxRepositoryChangesPageSize = client.endpointNumber("repository_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
432
|
+
const maxTargetExport = client.endpointNumber("target_export", "max_targets", MAX_TARGET_EXPORT);
|
|
433
|
+
const maxTargetExportProgramIds = client.endpointNumber("target_export", "max_program_ids", 50);
|
|
434
|
+
const programsPageSizeSchema = z.number().int().min(1).max(maxProgramsPageSize).default(Math.min(20, maxProgramsPageSize));
|
|
435
|
+
const recentChangesPageSizeSchema = z.number().int().min(1).max(maxTargetChangesPageSize).default(Math.min(50, maxTargetChangesPageSize));
|
|
436
|
+
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
437
|
+
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
438
|
+
registerTool("search_programs", {
|
|
371
439
|
title: "Search BBRadar Programs",
|
|
372
|
-
description: `
|
|
440
|
+
description: `Search or list active platform and Self-Hosted programs. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
|
|
373
441
|
inputSchema: {
|
|
442
|
+
search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
|
|
374
443
|
platforms: platformListSchema(),
|
|
375
444
|
tags: tagListSchema(),
|
|
376
445
|
updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
@@ -393,7 +462,7 @@ export function createBbradarServer(client, config) {
|
|
|
393
462
|
meta: sanitizeJson(data?.meta)
|
|
394
463
|
});
|
|
395
464
|
}));
|
|
396
|
-
|
|
465
|
+
registerTool("get_program", {
|
|
397
466
|
title: "Get BBRadar Program",
|
|
398
467
|
description: "Get one program by platform:handle program_id, handle, or human program name. Names are resolved automatically when unambiguous.",
|
|
399
468
|
inputSchema: {
|
|
@@ -429,7 +498,7 @@ export function createBbradarServer(client, config) {
|
|
|
429
498
|
}
|
|
430
499
|
}));
|
|
431
500
|
}));
|
|
432
|
-
|
|
501
|
+
registerTool("resolve_program", {
|
|
433
502
|
title: "Resolve Program",
|
|
434
503
|
description: "Resolve partial name/handle text to likely program_ids. Use this when the user gives a program name fragment like Nado.",
|
|
435
504
|
inputSchema: {
|
|
@@ -453,7 +522,7 @@ export function createBbradarServer(client, config) {
|
|
|
453
522
|
},
|
|
454
523
|
annotations: readOnlyAnnotations
|
|
455
524
|
}, (args) => runTool("resolve_program", config, rateLimiter, async () => resolveProgram(client, config, args)));
|
|
456
|
-
|
|
525
|
+
registerTool("run_program_name_action", {
|
|
457
526
|
title: "Run Program Name Action",
|
|
458
527
|
description: "Start here for one named program. Resolves a partial program name, then runs targets, new_targets, scope_delta, brief, or target_breakdown.",
|
|
459
528
|
inputSchema: {
|
|
@@ -467,8 +536,8 @@ export function createBbradarServer(client, config) {
|
|
|
467
536
|
language_tags: languageTagListSchema(),
|
|
468
537
|
include_out_of_scope: z.boolean().default(false),
|
|
469
538
|
include_ineligible: z.boolean().default(false),
|
|
470
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
471
|
-
max_targets: acceptedTargetLimitSchema.default(25),
|
|
539
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
540
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(25, maxProgramTargetsPageSize)),
|
|
472
541
|
group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
|
|
473
542
|
target_list_mode: targetListModeSchema.default("identifiers"),
|
|
474
543
|
max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
@@ -487,7 +556,7 @@ export function createBbradarServer(client, config) {
|
|
|
487
556
|
},
|
|
488
557
|
annotations: readOnlyAnnotations
|
|
489
558
|
}, (args) => runTool("run_program_name_action", config, rateLimiter, async () => runProgramNameAction(client, config, args)));
|
|
490
|
-
|
|
559
|
+
registerTool("get_program_eligible_targets_by_name", {
|
|
491
560
|
title: "Get Eligible Targets By Program Name",
|
|
492
561
|
description: "Fast one-call path for user asks like 'find in-scope eligible targets for Aztec': resolve the program name, fetch targets, and return filtered in-scope bounty-eligible targets. Prefer this over separate resolve_program + get_program_targets calls when the user asks for a target list by name.",
|
|
493
562
|
inputSchema: {
|
|
@@ -511,6 +580,7 @@ export function createBbradarServer(client, config) {
|
|
|
511
580
|
resolved_program: jsonRecordSchema().optional(),
|
|
512
581
|
program_id: z.string().optional(),
|
|
513
582
|
targets: z.array(z.unknown()).optional(),
|
|
583
|
+
continuation: jsonRecordSchema().optional(),
|
|
514
584
|
warnings: z.array(z.string()).optional(),
|
|
515
585
|
upstream_budget: jsonRecordSchema().optional(),
|
|
516
586
|
ranking_scope: jsonRecordSchema().optional(),
|
|
@@ -518,16 +588,21 @@ export function createBbradarServer(client, config) {
|
|
|
518
588
|
},
|
|
519
589
|
annotations: readOnlyAnnotations
|
|
520
590
|
}, (args) => runTool("get_program_eligible_targets_by_name", config, rateLimiter, async () => getProgramEligibleTargetsByName(client, config, args)));
|
|
521
|
-
|
|
591
|
+
registerTool("get_program_targets", {
|
|
522
592
|
title: "Get Program Targets",
|
|
523
|
-
description: `Get active targets for one program_id, handle, or human program name. This is a final source for eligible target lists; do not call summary/breakdown unless grouped counts are needed. For a program name query, get_program_eligible_targets_by_name is the simplest path. limit max ${
|
|
593
|
+
description: `Get active targets for one program_id, handle, or human program name. This is a final source for eligible target lists; do not call summary/breakdown unless grouped counts are needed. For a program name query, get_program_eligible_targets_by_name is the simplest path. limit max ${maxProgramTargetsPageSize}.`,
|
|
524
594
|
inputSchema: {
|
|
525
595
|
program_id: programIdentifierSchema,
|
|
596
|
+
search: searchTextSchema.optional(),
|
|
597
|
+
target_type: filterValueSchema().optional(),
|
|
598
|
+
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
599
|
+
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
600
|
+
competition_risk: competitionRiskSchema.optional(),
|
|
526
601
|
include_out_of_scope: z.boolean().default(false),
|
|
527
602
|
include_ineligible: z.boolean().default(false),
|
|
528
603
|
strict_scope_filter: z.boolean().default(true),
|
|
529
604
|
offset: z.number().int().min(0).default(0),
|
|
530
|
-
limit: z.number().int().min(1).max(
|
|
605
|
+
limit: z.number().int().min(1).max(maxProgramTargetsPageSize).default(Math.min(100, maxProgramTargetsPageSize)),
|
|
531
606
|
output_mode: targetListModeSchema.default("compact")
|
|
532
607
|
},
|
|
533
608
|
outputSchema: {
|
|
@@ -538,13 +613,21 @@ export function createBbradarServer(client, config) {
|
|
|
538
613
|
},
|
|
539
614
|
annotations: readOnlyAnnotations
|
|
540
615
|
}, (args) => runTool("get_program_targets", config, rateLimiter, async () => {
|
|
541
|
-
|
|
616
|
+
const payload = await runProgramIdTool(client, config, args.program_id, async (programId) => getProgramTargetsPayload(client, args, programId), async (indexFallback) => getProgramTargetsExportFallbackPayload(client, args, indexFallback.resolvedProgramId));
|
|
617
|
+
return applyCollectionContinuation(payload, "targets", "program targets", continuationStore, config);
|
|
542
618
|
}));
|
|
543
|
-
|
|
619
|
+
registerTool("get_recent_changes", {
|
|
544
620
|
title: "Get Recent Target Changes",
|
|
545
621
|
description: `List recent target changes. page_size max ${MAX_RECENT_CHANGES}.`,
|
|
546
622
|
inputSchema: {
|
|
547
623
|
change_type: z.enum(["added", "updated", "removed"]).optional(),
|
|
624
|
+
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns changes after this id in ascending order."),
|
|
625
|
+
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
626
|
+
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
627
|
+
cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
|
|
628
|
+
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
629
|
+
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
630
|
+
competition_risk: competitionRiskSchema.optional(),
|
|
548
631
|
include_removed: z.boolean().default(false),
|
|
549
632
|
include_ineligible: z.boolean().default(false),
|
|
550
633
|
include_out_of_scope: z.boolean().default(false),
|
|
@@ -559,35 +642,119 @@ export function createBbradarServer(client, config) {
|
|
|
559
642
|
...apiEnvelopeOutputShape(),
|
|
560
643
|
changes: z.array(jsonRecordSchema()).optional(),
|
|
561
644
|
meta: jsonRecordSchema().optional(),
|
|
562
|
-
facets: jsonRecordSchema().optional()
|
|
645
|
+
facets: jsonRecordSchema().optional(),
|
|
646
|
+
cursor: jsonRecordSchema().optional(),
|
|
647
|
+
continuation: jsonRecordSchema().optional()
|
|
563
648
|
},
|
|
564
649
|
annotations: readOnlyAnnotations
|
|
565
650
|
}, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
|
|
566
|
-
const { output_mode, ...filters } = args;
|
|
567
|
-
const
|
|
568
|
-
const
|
|
651
|
+
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
652
|
+
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor);
|
|
653
|
+
const { min_target_score, target_opportunity_label, competition_risk, ...upstreamFilters } = filters;
|
|
654
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRecentChanges(toQuery({
|
|
655
|
+
...upstreamFilters,
|
|
656
|
+
minTargetScore: min_target_score,
|
|
657
|
+
targetOpportunityLabel: target_opportunity_label,
|
|
658
|
+
competitionRisk: competition_risk,
|
|
659
|
+
after_id: afterId,
|
|
660
|
+
page: afterId === undefined ? filters.page : 1
|
|
661
|
+
})));
|
|
662
|
+
const api = collected.lastApi;
|
|
663
|
+
const data = collected.lastData;
|
|
569
664
|
const sanitizedMeta = readObject(sanitizeJson(data?.meta));
|
|
570
|
-
const changes =
|
|
665
|
+
const changes = collected.results
|
|
571
666
|
.map((change) => sanitizeChange(change, config.webBaseUrl))
|
|
572
667
|
.map((change) => formatChange(change, output_mode));
|
|
573
|
-
return withApiMetadata(api, {
|
|
668
|
+
return applyCollectionContinuation(withApiMetadata(api, {
|
|
574
669
|
changes,
|
|
575
670
|
meta: stripUndefined({
|
|
576
671
|
...(sanitizedMeta ?? {}),
|
|
577
|
-
output_mode
|
|
672
|
+
output_mode,
|
|
673
|
+
pages_fetched: collected.pagesFetched,
|
|
674
|
+
returned_across_pages: changes.length,
|
|
675
|
+
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
676
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
578
677
|
}),
|
|
579
|
-
facets: sanitizeJson(data?.facets)
|
|
580
|
-
|
|
678
|
+
facets: sanitizeJson(data?.facets),
|
|
679
|
+
cursor: collected.cursor,
|
|
680
|
+
source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
|
|
681
|
+
}), "changes", "target changes", continuationStore, config);
|
|
682
|
+
}));
|
|
683
|
+
registerTool("get_repository_changes", {
|
|
684
|
+
title: "Get Repository Changes",
|
|
685
|
+
description: "Read-only repository push/change feed. Defaults to lightweight program summaries; use after_id for efficient incremental polling. For rewarded Web3 repository targets, call this in parallel with find_programs using the same platforms, then intersect event program ids with the ranked rewarded programs.",
|
|
686
|
+
inputSchema: {
|
|
687
|
+
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns events after this id in ascending order."),
|
|
688
|
+
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
689
|
+
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
690
|
+
cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
|
|
691
|
+
include_targets: z.boolean().default(false).describe("Include matched targets. False is faster and returns affected programs only."),
|
|
692
|
+
search: searchTextSchema.optional(),
|
|
693
|
+
platforms: platformListSchema(),
|
|
694
|
+
tags: tagListSchema(),
|
|
695
|
+
free_submission_only: z.boolean().default(false),
|
|
696
|
+
include_removed: z.boolean().default(false),
|
|
697
|
+
include_ineligible: z.boolean().default(false),
|
|
698
|
+
include_out_of_scope: z.boolean().default(false),
|
|
699
|
+
page: pageSchema,
|
|
700
|
+
page_size: repositoryChangesPageSizeSchema,
|
|
701
|
+
output_mode: repositoryChangeListModeSchema.default("compact")
|
|
702
|
+
},
|
|
703
|
+
outputSchema: {
|
|
704
|
+
...apiEnvelopeOutputShape(),
|
|
705
|
+
repository_changes: z.array(jsonRecordSchema()).optional(),
|
|
706
|
+
meta: jsonRecordSchema().optional(),
|
|
707
|
+
facets: jsonRecordSchema().optional(),
|
|
708
|
+
available_facets: jsonRecordSchema().optional(),
|
|
709
|
+
cursor: jsonRecordSchema().optional(),
|
|
710
|
+
continuation: jsonRecordSchema().optional()
|
|
711
|
+
},
|
|
712
|
+
annotations: readOnlyAnnotations
|
|
713
|
+
}, (args) => runTool("get_repository_changes", config, rateLimiter, async () => {
|
|
714
|
+
if (!client.supportsEndpoint("repository_changes")) {
|
|
715
|
+
throw new Error("The negotiated BBRadar API contract does not expose repository changes.");
|
|
716
|
+
}
|
|
717
|
+
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
718
|
+
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor);
|
|
719
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRepositoryChanges(toQuery({
|
|
720
|
+
...filters,
|
|
721
|
+
after_id: afterId,
|
|
722
|
+
page: afterId === undefined ? filters.page : 1
|
|
723
|
+
})));
|
|
724
|
+
const api = collected.lastApi;
|
|
725
|
+
const data = collected.lastData;
|
|
726
|
+
return applyCollectionContinuation(withApiMetadata(api, {
|
|
727
|
+
repository_changes: collected.results.map((change) => sanitizeRepositoryChange(change, config.webBaseUrl)).map((change) => formatRepositoryChange(change, output_mode)),
|
|
728
|
+
meta: stripUndefined({
|
|
729
|
+
...(readObject(sanitizeJson(data?.meta)) ?? {}),
|
|
730
|
+
output_mode,
|
|
731
|
+
pages_fetched: collected.pagesFetched,
|
|
732
|
+
returned_across_pages: collected.results.length,
|
|
733
|
+
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
734
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
735
|
+
}),
|
|
736
|
+
facets: sanitizeJson(data?.facets),
|
|
737
|
+
available_facets: sanitizeJson(data?.available_facets),
|
|
738
|
+
cursor: collected.cursor,
|
|
739
|
+
source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
|
|
740
|
+
}), "repository_changes", "repository changes", continuationStore, config);
|
|
581
741
|
}));
|
|
582
|
-
|
|
742
|
+
registerTool("get_latest_added_targets", {
|
|
583
743
|
title: "Get Latest Added Targets",
|
|
584
|
-
description: "Latest added targets by type,
|
|
744
|
+
description: "Latest added targets by type with optional platform, reward, and report-count filters. For fast wildcard discovery use target_type=wildcard and include_full_target_list=false.",
|
|
585
745
|
inputSchema: {
|
|
586
746
|
target_type: filterValueSchema().default("domain"),
|
|
747
|
+
platforms: platformListSchema(),
|
|
748
|
+
min_reward: z.number().int().min(0).optional(),
|
|
749
|
+
reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
|
|
750
|
+
max_public_report_count: z.number().int().min(0).optional(),
|
|
751
|
+
require_known_report_count: z.boolean().default(false),
|
|
752
|
+
include_unknown_report_count: z.boolean().default(true),
|
|
587
753
|
include_out_of_scope: z.boolean().default(false).describe("Include out-of-scope."),
|
|
588
754
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
589
|
-
recent_changes_page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
590
|
-
|
|
755
|
+
recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
756
|
+
max_results: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(10),
|
|
757
|
+
include_full_target_list: z.boolean().default(true).describe("Fetch the newest matching program's complete target list. Set false for the fastest cross-program discovery."),
|
|
591
758
|
full_target_list_mode: targetListModeSchema.default("identifiers"),
|
|
592
759
|
full_target_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(MAX_TARGETS_PER_PROGRAM),
|
|
593
760
|
full_list_include_out_of_scope: z.boolean().default(false),
|
|
@@ -599,13 +766,14 @@ export function createBbradarServer(client, config) {
|
|
|
599
766
|
query: jsonRecordSchema().optional(),
|
|
600
767
|
latest_added_at: z.string().optional(),
|
|
601
768
|
program: programOutputSchema().optional(),
|
|
769
|
+
matches: z.array(jsonRecordSchema()).optional(),
|
|
602
770
|
new_targets: z.array(z.unknown()).optional(),
|
|
603
771
|
full_targets: z.array(z.unknown()).optional(),
|
|
604
772
|
meta: jsonRecordSchema().optional()
|
|
605
773
|
},
|
|
606
774
|
annotations: readOnlyAnnotations
|
|
607
775
|
}, (args) => runTool("get_latest_added_targets", config, rateLimiter, async () => getLatestAddedTargets(client, config, args)));
|
|
608
|
-
|
|
776
|
+
registerTool("get_program_scope_summary", {
|
|
609
777
|
title: "Get Program Scope Summary",
|
|
610
778
|
description: "Grouped compact scope for one program.",
|
|
611
779
|
inputSchema: {
|
|
@@ -624,7 +792,7 @@ export function createBbradarServer(client, config) {
|
|
|
624
792
|
},
|
|
625
793
|
annotations: readOnlyAnnotations
|
|
626
794
|
}, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeSummary(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramScopeSummaryFromIndexFallback(client, indexFallback, { ...args, program_id: indexFallback.resolvedProgramId }))));
|
|
627
|
-
|
|
795
|
+
registerTool("get_program_target_breakdown", {
|
|
628
796
|
title: "Get Program Target Breakdown",
|
|
629
797
|
description: "Target type, scope tag, language tag, and bucket counts for one program.",
|
|
630
798
|
inputSchema: {
|
|
@@ -643,7 +811,7 @@ export function createBbradarServer(client, config) {
|
|
|
643
811
|
},
|
|
644
812
|
annotations: readOnlyAnnotations
|
|
645
813
|
}, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramTargetBreakdown(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramTargetBreakdownFromIndexFallback(client, indexFallback, { ...args, program_id: indexFallback.resolvedProgramId }))));
|
|
646
|
-
|
|
814
|
+
registerTool("get_program_scope_delta", {
|
|
647
815
|
title: "Get Program Scope Delta",
|
|
648
816
|
description: "Compact recent scope changes for one program.",
|
|
649
817
|
inputSchema: {
|
|
@@ -655,8 +823,8 @@ export function createBbradarServer(client, config) {
|
|
|
655
823
|
include_removed: z.boolean().default(true),
|
|
656
824
|
include_out_of_scope: z.boolean().default(false),
|
|
657
825
|
include_ineligible: z.boolean().default(false),
|
|
658
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
659
|
-
max_targets: acceptedTargetLimitSchema.default(50),
|
|
826
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
827
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(50, maxProgramTargetsPageSize)),
|
|
660
828
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
661
829
|
},
|
|
662
830
|
outputSchema: {
|
|
@@ -669,7 +837,7 @@ export function createBbradarServer(client, config) {
|
|
|
669
837
|
},
|
|
670
838
|
annotations: readOnlyAnnotations
|
|
671
839
|
}, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeDelta(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramScopeDeltaFromIndexedProgram(client, config, { ...args, program_id: indexFallback.resolvedProgramId }, indexFallback.program))));
|
|
672
|
-
|
|
840
|
+
registerTool("get_recent_target_activity", {
|
|
673
841
|
title: "Get Recent Target Activity",
|
|
674
842
|
description: "Recent target changes grouped by program.",
|
|
675
843
|
inputSchema: {
|
|
@@ -682,7 +850,7 @@ export function createBbradarServer(client, config) {
|
|
|
682
850
|
platforms: platformListSchema(),
|
|
683
851
|
tags: tagListSchema(),
|
|
684
852
|
language_tags: languageTagListSchema(),
|
|
685
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
853
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
686
854
|
max_programs: z.number().int().min(1).max(50).default(10),
|
|
687
855
|
sample_size: z.number().int().min(1).max(20).default(5),
|
|
688
856
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
@@ -695,7 +863,7 @@ export function createBbradarServer(client, config) {
|
|
|
695
863
|
},
|
|
696
864
|
annotations: readOnlyAnnotations
|
|
697
865
|
}, (args) => runTool("get_recent_target_activity", config, rateLimiter, async () => getRecentTargetActivity(client, config, args)));
|
|
698
|
-
|
|
866
|
+
registerTool("check_program_new_targets", {
|
|
699
867
|
title: "Check Program New Targets",
|
|
700
868
|
description: "Yes/no new in-scope targets for one program.",
|
|
701
869
|
inputSchema: {
|
|
@@ -703,8 +871,8 @@ export function createBbradarServer(client, config) {
|
|
|
703
871
|
since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
704
872
|
target_type: filterValueSchema().optional(),
|
|
705
873
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
706
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
707
|
-
max_targets: acceptedTargetLimitSchema.default(25),
|
|
874
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
875
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(25, maxProgramTargetsPageSize)),
|
|
708
876
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
709
877
|
},
|
|
710
878
|
outputSchema: {
|
|
@@ -720,7 +888,7 @@ export function createBbradarServer(client, config) {
|
|
|
720
888
|
},
|
|
721
889
|
annotations: readOnlyAnnotations
|
|
722
890
|
}, (args) => runTool("check_program_new_targets", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => checkProgramNewTargets(client, config, { ...args, program_id: programId }), (indexFallback) => checkProgramNewTargetsFromIndexedProgram(client, config, { ...args, program_id: indexFallback.resolvedProgramId }, indexFallback.program))));
|
|
723
|
-
|
|
891
|
+
registerTool("check_watchlist_new_targets", {
|
|
724
892
|
title: "Check Watchlist New Targets",
|
|
725
893
|
description: "New in-scope targets across many known program_ids.",
|
|
726
894
|
inputSchema: {
|
|
@@ -728,7 +896,7 @@ export function createBbradarServer(client, config) {
|
|
|
728
896
|
since: isoDateTimeSchema.optional(),
|
|
729
897
|
target_type: filterValueSchema().optional(),
|
|
730
898
|
include_ineligible: z.boolean().default(false),
|
|
731
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
899
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
732
900
|
max_targets_per_program: z.number().int().min(1).max(50).default(10),
|
|
733
901
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
734
902
|
},
|
|
@@ -741,7 +909,7 @@ export function createBbradarServer(client, config) {
|
|
|
741
909
|
},
|
|
742
910
|
annotations: readOnlyAnnotations
|
|
743
911
|
}, (args) => runTool("check_watchlist_new_targets", config, rateLimiter, async () => checkWatchlistNewTargets(client, config, args)));
|
|
744
|
-
|
|
912
|
+
registerTool("get_opportunities", {
|
|
745
913
|
title: "Get BBRadar Opportunities",
|
|
746
914
|
description: `List programs in one opportunity tier. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
|
|
747
915
|
inputSchema: {
|
|
@@ -772,16 +940,18 @@ export function createBbradarServer(client, config) {
|
|
|
772
940
|
meta: sanitizeJson(data?.meta)
|
|
773
941
|
});
|
|
774
942
|
}));
|
|
775
|
-
|
|
943
|
+
registerTool("find_programs", {
|
|
776
944
|
title: "Find BBRadar Programs",
|
|
777
|
-
description: "Ranked program discovery with filters and compact output.",
|
|
945
|
+
description: "Ranked program discovery with positive and negative platform/surface filters and compact output. Strict-filter retries reuse cached upstream pages, so relax one local filter at a time when a narrow query returns no candidates.",
|
|
778
946
|
inputSchema: {
|
|
779
947
|
platforms: platformListSchema(),
|
|
948
|
+
exclude_platforms: stringListSchema("Platforms to exclude after retrieval. Use list_filters first when the user asks for a geographic/platform exclusion without naming exact platform values."),
|
|
780
949
|
tags: tagListSchema(),
|
|
781
950
|
scope_tags: scopeTagListSchema(),
|
|
782
951
|
target_types: targetTypeListSchema(),
|
|
783
952
|
language_tags: languageTagListSchema(),
|
|
784
953
|
web3: z.boolean().default(false).describe("Require Web3 signals."),
|
|
954
|
+
exclude_web3: z.boolean().default(false).describe("Exclude programs with Web3, blockchain, smart-contract, crypto, DeFi, NFT, Solidity, Ethereum, or Solana signals. Use this for Web2-only discovery."),
|
|
785
955
|
opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
|
|
786
956
|
updated_since: isoDateTimeSchema.optional(),
|
|
787
957
|
min_bounty_min: z.number().int().min(0).optional(),
|
|
@@ -820,24 +990,30 @@ export function createBbradarServer(client, config) {
|
|
|
820
990
|
...args,
|
|
821
991
|
target_samples_only_wildcards: args.min_wildcards > 0
|
|
822
992
|
})));
|
|
823
|
-
|
|
993
|
+
registerTool("find_wildcard_programs", {
|
|
824
994
|
title: "Find Wildcard Programs",
|
|
825
|
-
description: "Wildcard program shortlist.",
|
|
995
|
+
description: "Wildcard program shortlist with platform, minimum reward, freshness, and report-count filters.",
|
|
826
996
|
inputSchema: {
|
|
827
997
|
platforms: platformListSchema(),
|
|
828
998
|
tags: tagListSchema(),
|
|
829
999
|
updated_since: isoDateTimeSchema.optional(),
|
|
1000
|
+
min_reward: z.number().int().min(0).optional(),
|
|
1001
|
+
reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
|
|
830
1002
|
max_public_report_count: z.number().int().min(0).default(25),
|
|
831
1003
|
require_known_report_count: z.boolean().default(true),
|
|
832
1004
|
include_unknown_report_count: z.boolean().default(false),
|
|
833
1005
|
exclude_ctf: z.boolean().default(true),
|
|
834
1006
|
require_bounty: z.boolean().default(true),
|
|
835
1007
|
min_wildcards: z.number().int().min(1).max(100).default(1),
|
|
1008
|
+
min_added_24h: z.number().int().min(0).optional(),
|
|
1009
|
+
min_added_7d: z.number().int().min(0).optional(),
|
|
1010
|
+
sort_by: programSortSchema.default("best_hunt_value"),
|
|
836
1011
|
max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
837
1012
|
max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
|
|
838
1013
|
include_target_samples: z.boolean().default(false),
|
|
839
1014
|
target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
840
1015
|
target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
|
|
1016
|
+
output_mode: programListModeSchema.default("compact"),
|
|
841
1017
|
upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
|
|
842
1018
|
},
|
|
843
1019
|
outputSchema: findProgramsOutputShape(),
|
|
@@ -851,8 +1027,8 @@ export function createBbradarServer(client, config) {
|
|
|
851
1027
|
web3: false,
|
|
852
1028
|
opportunity_levels: [],
|
|
853
1029
|
updated_since: args.updated_since,
|
|
854
|
-
min_bounty_min: undefined,
|
|
855
|
-
min_bounty_max: undefined,
|
|
1030
|
+
min_bounty_min: args.min_reward !== undefined && args.reward_threshold_mode === "min_at_least" ? args.min_reward : undefined,
|
|
1031
|
+
min_bounty_max: args.min_reward !== undefined && args.reward_threshold_mode === "max_at_least" ? args.min_reward : undefined,
|
|
856
1032
|
max_bounty_max: undefined,
|
|
857
1033
|
require_bounty: args.require_bounty,
|
|
858
1034
|
max_public_report_count: args.max_public_report_count,
|
|
@@ -867,20 +1043,21 @@ export function createBbradarServer(client, config) {
|
|
|
867
1043
|
min_wildcards: args.min_wildcards,
|
|
868
1044
|
min_eligible_targets: undefined,
|
|
869
1045
|
min_total_targets: undefined,
|
|
870
|
-
min_added_24h:
|
|
871
|
-
min_added_7d:
|
|
1046
|
+
min_added_24h: args.min_added_24h,
|
|
1047
|
+
min_added_7d: args.min_added_7d,
|
|
872
1048
|
max_pages: args.max_pages,
|
|
873
1049
|
max_results: args.max_results,
|
|
874
|
-
sort_by:
|
|
1050
|
+
sort_by: args.sort_by,
|
|
875
1051
|
include_target_samples: args.include_target_samples,
|
|
876
1052
|
target_sample_programs: args.target_sample_programs,
|
|
877
1053
|
target_sample_size: args.target_sample_size,
|
|
878
1054
|
only_in_scope_targets: true,
|
|
879
1055
|
only_bounty_eligible_targets: true,
|
|
880
1056
|
target_samples_only_wildcards: true,
|
|
1057
|
+
output_mode: args.output_mode,
|
|
881
1058
|
upstream_request_budget: args.upstream_request_budget
|
|
882
1059
|
})));
|
|
883
|
-
|
|
1060
|
+
registerTool("find_web3_programs", {
|
|
884
1061
|
title: "Find Web3 Programs",
|
|
885
1062
|
description: "Web3/blockchain/smart-contract program shortlist.",
|
|
886
1063
|
inputSchema: {
|
|
@@ -941,7 +1118,7 @@ export function createBbradarServer(client, config) {
|
|
|
941
1118
|
target_samples_only_wildcards: args.min_wildcards > 0,
|
|
942
1119
|
upstream_request_budget: args.upstream_request_budget
|
|
943
1120
|
})));
|
|
944
|
-
|
|
1121
|
+
registerTool("find_reward_programs", {
|
|
945
1122
|
title: "Find Reward Programs",
|
|
946
1123
|
description: "Programs matching reward thresholds.",
|
|
947
1124
|
inputSchema: {
|
|
@@ -970,7 +1147,7 @@ export function createBbradarServer(client, config) {
|
|
|
970
1147
|
outputSchema: findProgramsOutputShape(),
|
|
971
1148
|
annotations: readOnlyAnnotations
|
|
972
1149
|
}, (args) => runTool("find_reward_programs", config, rateLimiter, async () => findRewardPrograms(client, config, args)));
|
|
973
|
-
|
|
1150
|
+
registerTool("find_web3_contests", {
|
|
974
1151
|
title: "Find Web3 Contests",
|
|
975
1152
|
description: "Web3 contest or competitive audit shortlist.",
|
|
976
1153
|
inputSchema: {
|
|
@@ -993,7 +1170,7 @@ export function createBbradarServer(client, config) {
|
|
|
993
1170
|
outputSchema: findProgramsOutputShape(),
|
|
994
1171
|
annotations: readOnlyAnnotations
|
|
995
1172
|
}, (args) => runTool("find_web3_contests", config, rateLimiter, async () => findWeb3Contests(client, config, args)));
|
|
996
|
-
|
|
1173
|
+
registerTool("find_language_programs", {
|
|
997
1174
|
title: "Find Language Programs",
|
|
998
1175
|
description: "Language-specific program shortlist.",
|
|
999
1176
|
inputSchema: {
|
|
@@ -1020,7 +1197,7 @@ export function createBbradarServer(client, config) {
|
|
|
1020
1197
|
outputSchema: findProgramsOutputShape(),
|
|
1021
1198
|
annotations: readOnlyAnnotations
|
|
1022
1199
|
}, (args) => runTool("find_language_programs", config, rateLimiter, async () => findLanguagePrograms(client, config, args)));
|
|
1023
|
-
|
|
1200
|
+
registerTool("find_target_type_programs", {
|
|
1024
1201
|
title: "Find Target Type Programs",
|
|
1025
1202
|
description: "Target-type program shortlist.",
|
|
1026
1203
|
inputSchema: {
|
|
@@ -1046,7 +1223,7 @@ export function createBbradarServer(client, config) {
|
|
|
1046
1223
|
outputSchema: findProgramsOutputShape(),
|
|
1047
1224
|
annotations: readOnlyAnnotations
|
|
1048
1225
|
}, (args) => runTool("find_target_type_programs", config, rateLimiter, async () => findTargetTypePrograms(client, config, args)));
|
|
1049
|
-
|
|
1226
|
+
registerTool("find_vdp_programs", {
|
|
1050
1227
|
title: "Find VDP Programs",
|
|
1051
1228
|
description: "VDP/disclosure-only/no-bounty program shortlist.",
|
|
1052
1229
|
inputSchema: {
|
|
@@ -1066,7 +1243,7 @@ export function createBbradarServer(client, config) {
|
|
|
1066
1243
|
outputSchema: findProgramsOutputShape(),
|
|
1067
1244
|
annotations: readOnlyAnnotations
|
|
1068
1245
|
}, (args) => runTool("find_vdp_programs", config, rateLimiter, async () => findVdpPrograms(client, config, args)));
|
|
1069
|
-
|
|
1246
|
+
registerTool("find_paid_programs", {
|
|
1070
1247
|
title: "Find Paid Programs",
|
|
1071
1248
|
description: "Paid bounty program shortlist.",
|
|
1072
1249
|
inputSchema: {
|
|
@@ -1089,7 +1266,7 @@ export function createBbradarServer(client, config) {
|
|
|
1089
1266
|
outputSchema: findProgramsOutputShape(),
|
|
1090
1267
|
annotations: readOnlyAnnotations
|
|
1091
1268
|
}, (args) => runTool("find_paid_programs", config, rateLimiter, async () => findPaidPrograms(client, config, args)));
|
|
1092
|
-
|
|
1269
|
+
registerTool("find_stack_matches", {
|
|
1093
1270
|
title: "Find Stack Matches",
|
|
1094
1271
|
description: "Combined stack/language/type/reward shortlist.",
|
|
1095
1272
|
inputSchema: {
|
|
@@ -1119,7 +1296,7 @@ export function createBbradarServer(client, config) {
|
|
|
1119
1296
|
outputSchema: findProgramsOutputShape(),
|
|
1120
1297
|
annotations: readOnlyAnnotations
|
|
1121
1298
|
}, (args) => runTool("find_stack_matches", config, rateLimiter, async () => findStackMatches(client, config, args)));
|
|
1122
|
-
|
|
1299
|
+
registerTool("find_low_noise_programs", {
|
|
1123
1300
|
title: "Find Low Noise Programs",
|
|
1124
1301
|
description: "Low-public-report program shortlist.",
|
|
1125
1302
|
inputSchema: {
|
|
@@ -1145,7 +1322,7 @@ export function createBbradarServer(client, config) {
|
|
|
1145
1322
|
outputSchema: findProgramsOutputShape(),
|
|
1146
1323
|
annotations: readOnlyAnnotations
|
|
1147
1324
|
}, (args) => runTool("find_low_noise_programs", config, rateLimiter, async () => findLowNoisePrograms(client, config, args)));
|
|
1148
|
-
|
|
1325
|
+
registerTool("find_recent_by_type", {
|
|
1149
1326
|
title: "Find Recent By Type",
|
|
1150
1327
|
description: "Recent changes by target type grouped by program.",
|
|
1151
1328
|
inputSchema: {
|
|
@@ -1158,7 +1335,7 @@ export function createBbradarServer(client, config) {
|
|
|
1158
1335
|
platforms: platformListSchema(),
|
|
1159
1336
|
tags: tagListSchema(),
|
|
1160
1337
|
language_tags: languageTagListSchema(),
|
|
1161
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
1338
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
1162
1339
|
max_programs: z.number().int().min(1).max(50).default(10),
|
|
1163
1340
|
sample_size: z.number().int().min(1).max(20).default(5),
|
|
1164
1341
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
@@ -1171,7 +1348,7 @@ export function createBbradarServer(client, config) {
|
|
|
1171
1348
|
},
|
|
1172
1349
|
annotations: readOnlyAnnotations
|
|
1173
1350
|
}, (args) => runTool("find_recent_by_type", config, rateLimiter, async () => findRecentByType(client, config, args)));
|
|
1174
|
-
|
|
1351
|
+
registerTool("find_programs_by_target", {
|
|
1175
1352
|
title: "Find Programs By Target",
|
|
1176
1353
|
description: "Reverse lookup target/domain ownership. Accepts domains, URLs, wildcards, target names, and partial target text.",
|
|
1177
1354
|
inputSchema: {
|
|
@@ -1194,21 +1371,21 @@ export function createBbradarServer(client, config) {
|
|
|
1194
1371
|
},
|
|
1195
1372
|
annotations: readOnlyAnnotations
|
|
1196
1373
|
}, (args) => runTool("find_programs_by_target", config, rateLimiter, async () => findProgramsByTarget(client, config, args)));
|
|
1197
|
-
|
|
1374
|
+
registerTool("find_program_candidates", {
|
|
1198
1375
|
title: "Find BBRadar Program Candidates",
|
|
1199
1376
|
description: "Start here for broad program discovery. Default ranked hunt-candidate preset with safe filters and compact output.",
|
|
1200
1377
|
inputSchema: programCandidatePresetInputSchema,
|
|
1201
1378
|
outputSchema: findProgramsOutputShape(),
|
|
1202
1379
|
annotations: readOnlyAnnotations
|
|
1203
1380
|
}, (args) => runTool("find_program_candidates", config, rateLimiter, async () => findProgramCandidates(client, config, args)));
|
|
1204
|
-
|
|
1381
|
+
registerTool("find_hunt_candidates", {
|
|
1205
1382
|
title: "Find BBRadar Hunt Candidates",
|
|
1206
1383
|
description: "Legacy alias for find_program_candidates. Prefer find_program_candidates for new calls.",
|
|
1207
1384
|
inputSchema: programCandidatePresetInputSchema,
|
|
1208
1385
|
outputSchema: findProgramsOutputShape(),
|
|
1209
1386
|
annotations: readOnlyAnnotations
|
|
1210
1387
|
}, (args) => runTool("find_hunt_candidates", config, rateLimiter, async () => findProgramCandidates(client, config, args)));
|
|
1211
|
-
|
|
1388
|
+
registerTool("find_fresh_hunt_candidates", {
|
|
1212
1389
|
title: "Find Fresh Hunt Candidates",
|
|
1213
1390
|
description: "Freshness-first hunt shortlist.",
|
|
1214
1391
|
inputSchema: {
|
|
@@ -1277,7 +1454,7 @@ export function createBbradarServer(client, config) {
|
|
|
1277
1454
|
output_mode: args.output_mode,
|
|
1278
1455
|
upstream_request_budget: args.upstream_request_budget
|
|
1279
1456
|
})));
|
|
1280
|
-
|
|
1457
|
+
registerTool("find_recently_updated_programs", {
|
|
1281
1458
|
title: "Find Recently Updated Programs",
|
|
1282
1459
|
description: "Recently updated program shortlist across all platforms by default. Platform filters are applied only when explicitly provided.",
|
|
1283
1460
|
inputSchema: {
|
|
@@ -1302,7 +1479,7 @@ export function createBbradarServer(client, config) {
|
|
|
1302
1479
|
outputSchema: findProgramsOutputShape(),
|
|
1303
1480
|
annotations: readOnlyAnnotations
|
|
1304
1481
|
}, (args) => runTool("find_recently_updated_programs", config, rateLimiter, async () => findRecentlyUpdatedPrograms(client, config, args)));
|
|
1305
|
-
|
|
1482
|
+
registerTool("list_filters", {
|
|
1306
1483
|
title: "List BBRadar Filters",
|
|
1307
1484
|
description: "Discover observed filter values.",
|
|
1308
1485
|
inputSchema: {
|
|
@@ -1321,7 +1498,7 @@ export function createBbradarServer(client, config) {
|
|
|
1321
1498
|
},
|
|
1322
1499
|
annotations: readOnlyAnnotations
|
|
1323
1500
|
}, (args) => runTool("list_filters", config, rateLimiter, async () => listFilters(client, args)));
|
|
1324
|
-
|
|
1501
|
+
registerTool("get_bbradar_guide", {
|
|
1325
1502
|
title: "Get BBRadar Tool Guide",
|
|
1326
1503
|
description: "Local quick-start guide for choosing BBRadar tools. Call this when the user's request is ambiguous or when a smaller model may need a decision tree.",
|
|
1327
1504
|
inputSchema: {
|
|
@@ -1334,9 +1511,9 @@ export function createBbradarServer(client, config) {
|
|
|
1334
1511
|
annotations: readOnlyAnnotations
|
|
1335
1512
|
}, (args) => runTool("get_bbradar_guide", config, rateLimiter, async () => ({
|
|
1336
1513
|
request_id: randomUUID(),
|
|
1337
|
-
guide: bbradarGuide(args.topic)
|
|
1514
|
+
guide: bbradarGuide(args.topic, config.toolProfile, client)
|
|
1338
1515
|
})));
|
|
1339
|
-
|
|
1516
|
+
registerTool("get_mcp_status", {
|
|
1340
1517
|
title: "Get BBRadar MCP Status",
|
|
1341
1518
|
description: "Local MCP status and timing metrics.",
|
|
1342
1519
|
inputSchema: {},
|
|
@@ -1355,11 +1532,22 @@ export function createBbradarServer(client, config) {
|
|
|
1355
1532
|
web_base_url: config.webBaseUrl,
|
|
1356
1533
|
response_cache_enabled: config.cacheTtlMs > 0 && config.cacheMaxEntries > 0,
|
|
1357
1534
|
cache_ttl_ms: config.cacheTtlMs,
|
|
1535
|
+
negative_cache_ttl_ms: config.negativeCacheTtlMs,
|
|
1358
1536
|
cache_max_entries: config.cacheMaxEntries,
|
|
1359
1537
|
local_program_index_enabled: false,
|
|
1360
1538
|
default_rate_limit_per_minute: config.defaultRateLimitPerMinute,
|
|
1361
1539
|
export_rate_limit_per_minute: config.exportRateLimitPerMinute,
|
|
1540
|
+
max_concurrent_upstream_requests: config.maxConcurrentRequests,
|
|
1541
|
+
compact_tool_text: config.compactToolText,
|
|
1542
|
+
tool_profile: config.toolProfile,
|
|
1362
1543
|
request_timeout_ms: config.requestTimeoutMs,
|
|
1544
|
+
feed_cursors: feedCursorStore.stats(),
|
|
1545
|
+
continuations: {
|
|
1546
|
+
entries: continuationStore.size,
|
|
1547
|
+
max_entries: MAX_LOCAL_EXPORT_RESOURCES,
|
|
1548
|
+
inline_limit: config.continuationInlineLimit ?? 50,
|
|
1549
|
+
ttl_ms: config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS
|
|
1550
|
+
},
|
|
1363
1551
|
validate_on_startup: config.validateOnStartup,
|
|
1364
1552
|
transport: "stdio",
|
|
1365
1553
|
remote_streamable_http: "not_enabled_for_local_setup",
|
|
@@ -1367,7 +1555,7 @@ export function createBbradarServer(client, config) {
|
|
|
1367
1555
|
client: client.diagnostics()
|
|
1368
1556
|
}
|
|
1369
1557
|
})));
|
|
1370
|
-
|
|
1558
|
+
registerTool("compare_programs", {
|
|
1371
1559
|
title: "Compare BBRadar Programs",
|
|
1372
1560
|
description: "Compare programs.",
|
|
1373
1561
|
inputSchema: {
|
|
@@ -1381,7 +1569,7 @@ export function createBbradarServer(client, config) {
|
|
|
1381
1569
|
outputSchema: decisionOutputShape(),
|
|
1382
1570
|
annotations: readOnlyAnnotations
|
|
1383
1571
|
}, (args) => runTool("compare_programs", config, rateLimiter, async () => comparePrograms(client, config, args)));
|
|
1384
|
-
|
|
1572
|
+
registerTool("compare_programs_compact", {
|
|
1385
1573
|
title: "Compare Programs Compact",
|
|
1386
1574
|
description: "Compact program comparison.",
|
|
1387
1575
|
inputSchema: {
|
|
@@ -1394,7 +1582,7 @@ export function createBbradarServer(client, config) {
|
|
|
1394
1582
|
outputSchema: decisionOutputShape(),
|
|
1395
1583
|
annotations: readOnlyAnnotations
|
|
1396
1584
|
}, (args) => runTool("compare_programs_compact", config, rateLimiter, async () => comparePrograms(client, config, { ...args, output_mode: "compact" })));
|
|
1397
|
-
|
|
1585
|
+
registerTool("summarize_program_activity", {
|
|
1398
1586
|
title: "Summarize Program Activity",
|
|
1399
1587
|
description: "Summarize one program's activity.",
|
|
1400
1588
|
inputSchema: {
|
|
@@ -1409,7 +1597,7 @@ export function createBbradarServer(client, config) {
|
|
|
1409
1597
|
},
|
|
1410
1598
|
annotations: readOnlyAnnotations
|
|
1411
1599
|
}, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => summarizeProgramActivity(client, config, programId, args.recent_changes_limit))));
|
|
1412
|
-
|
|
1600
|
+
registerTool("get_program_brief", {
|
|
1413
1601
|
title: "Get Program Brief",
|
|
1414
1602
|
description: "Compact worth-hunting brief for one program.",
|
|
1415
1603
|
inputSchema: {
|
|
@@ -1432,17 +1620,28 @@ export function createBbradarServer(client, config) {
|
|
|
1432
1620
|
},
|
|
1433
1621
|
annotations: readOnlyAnnotations
|
|
1434
1622
|
}, (args) => runTool("get_program_brief", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramBrief(client, config, { ...args, program_id: programId }))));
|
|
1435
|
-
|
|
1623
|
+
registerTool("find_recently_added_wildcards", {
|
|
1436
1624
|
title: "Find Recently Added Wildcards",
|
|
1437
|
-
description: "Recently added wildcard programs.",
|
|
1625
|
+
description: "Recently added wildcard programs with platform, minimum reward, and report-count filters. Returns bounded wildcard target samples.",
|
|
1438
1626
|
inputSchema: {
|
|
1439
1627
|
platforms: platformListSchema(),
|
|
1440
1628
|
tags: tagListSchema(),
|
|
1629
|
+
min_added_24h: z.number().int().min(0).optional(),
|
|
1441
1630
|
min_added_7d: z.number().int().min(1).default(1),
|
|
1631
|
+
min_reward: z.number().int().min(0).optional(),
|
|
1632
|
+
reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
|
|
1442
1633
|
max_public_report_count: z.number().int().min(0).optional(),
|
|
1634
|
+
require_known_report_count: z.boolean().default(false),
|
|
1635
|
+
include_unknown_report_count: z.boolean().default(false),
|
|
1443
1636
|
require_bounty: z.boolean().default(true),
|
|
1637
|
+
min_wildcards: z.number().int().min(1).max(100).default(1),
|
|
1638
|
+
sort_by: programSortSchema.default("most_new_targets"),
|
|
1444
1639
|
max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1445
1640
|
max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
|
|
1641
|
+
include_target_samples: z.boolean().default(true),
|
|
1642
|
+
target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1643
|
+
target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
|
|
1644
|
+
output_mode: programListModeSchema.default("compact"),
|
|
1446
1645
|
upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
|
|
1447
1646
|
},
|
|
1448
1647
|
outputSchema: findProgramsOutputShape(),
|
|
@@ -1456,37 +1655,38 @@ export function createBbradarServer(client, config) {
|
|
|
1456
1655
|
web3: false,
|
|
1457
1656
|
opportunity_levels: [],
|
|
1458
1657
|
updated_since: undefined,
|
|
1459
|
-
min_bounty_min: undefined,
|
|
1460
|
-
min_bounty_max: undefined,
|
|
1658
|
+
min_bounty_min: args.min_reward !== undefined && args.reward_threshold_mode === "min_at_least" ? args.min_reward : undefined,
|
|
1659
|
+
min_bounty_max: args.min_reward !== undefined && args.reward_threshold_mode === "max_at_least" ? args.min_reward : undefined,
|
|
1461
1660
|
max_bounty_max: undefined,
|
|
1462
1661
|
require_bounty: args.require_bounty,
|
|
1463
1662
|
max_public_report_count: args.max_public_report_count,
|
|
1464
|
-
require_known_report_count:
|
|
1465
|
-
include_unknown_report_count:
|
|
1663
|
+
require_known_report_count: args.require_known_report_count,
|
|
1664
|
+
include_unknown_report_count: args.include_unknown_report_count,
|
|
1466
1665
|
exclude_ctf: true,
|
|
1467
1666
|
exclude_mobile_only: false,
|
|
1468
1667
|
exclude_non_web: false,
|
|
1469
1668
|
has_api_scope: false,
|
|
1470
1669
|
fresh_launch_days: undefined,
|
|
1471
|
-
min_wildcards:
|
|
1670
|
+
min_wildcards: args.min_wildcards,
|
|
1472
1671
|
min_eligible_targets: undefined,
|
|
1473
1672
|
min_total_targets: undefined,
|
|
1474
|
-
min_added_24h:
|
|
1673
|
+
min_added_24h: args.min_added_24h,
|
|
1475
1674
|
min_added_7d: args.min_added_7d,
|
|
1476
1675
|
max_pages: args.max_pages,
|
|
1477
1676
|
max_results: args.max_results,
|
|
1478
|
-
sort_by:
|
|
1479
|
-
include_target_samples:
|
|
1480
|
-
target_sample_programs:
|
|
1481
|
-
target_sample_size:
|
|
1677
|
+
sort_by: args.sort_by,
|
|
1678
|
+
include_target_samples: args.include_target_samples,
|
|
1679
|
+
target_sample_programs: args.target_sample_programs,
|
|
1680
|
+
target_sample_size: args.target_sample_size,
|
|
1482
1681
|
only_in_scope_targets: true,
|
|
1483
1682
|
only_bounty_eligible_targets: true,
|
|
1484
1683
|
target_samples_only_wildcards: true,
|
|
1684
|
+
output_mode: args.output_mode,
|
|
1485
1685
|
upstream_request_budget: args.upstream_request_budget
|
|
1486
1686
|
})));
|
|
1487
|
-
|
|
1687
|
+
registerTool("find_low_competition_high_reward", {
|
|
1488
1688
|
title: "Find Low Competition High Reward",
|
|
1489
|
-
description: "
|
|
1689
|
+
description: "Low-report, high-reward shortlist with optional wildcard and technology filters.",
|
|
1490
1690
|
inputSchema: {
|
|
1491
1691
|
platforms: platformListSchema(),
|
|
1492
1692
|
tags: tagListSchema(),
|
|
@@ -1495,6 +1695,7 @@ export function createBbradarServer(client, config) {
|
|
|
1495
1695
|
max_public_report_count: z.number().int().min(0).default(10),
|
|
1496
1696
|
min_bounty_max: z.number().int().min(0).default(5_000),
|
|
1497
1697
|
min_eligible_targets: z.number().int().min(0).default(1),
|
|
1698
|
+
min_wildcards: z.number().int().min(0).max(100).default(0),
|
|
1498
1699
|
exclude_ctf: z.boolean().default(true),
|
|
1499
1700
|
exclude_mobile_only: z.boolean().default(false),
|
|
1500
1701
|
exclude_non_web: z.boolean().default(false),
|
|
@@ -1502,6 +1703,10 @@ export function createBbradarServer(client, config) {
|
|
|
1502
1703
|
fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
|
|
1503
1704
|
max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1504
1705
|
max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
|
|
1706
|
+
include_target_samples: z.boolean().default(true),
|
|
1707
|
+
target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1708
|
+
target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
|
|
1709
|
+
output_mode: programListModeSchema.default("compact"),
|
|
1505
1710
|
upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
|
|
1506
1711
|
},
|
|
1507
1712
|
outputSchema: findProgramsOutputShape(),
|
|
@@ -1527,7 +1732,7 @@ export function createBbradarServer(client, config) {
|
|
|
1527
1732
|
exclude_non_web: args.exclude_non_web,
|
|
1528
1733
|
has_api_scope: args.has_api_scope,
|
|
1529
1734
|
fresh_launch_days: args.fresh_launch_days,
|
|
1530
|
-
min_wildcards:
|
|
1735
|
+
min_wildcards: args.min_wildcards,
|
|
1531
1736
|
min_eligible_targets: args.min_eligible_targets,
|
|
1532
1737
|
min_total_targets: undefined,
|
|
1533
1738
|
min_added_24h: undefined,
|
|
@@ -1535,15 +1740,16 @@ export function createBbradarServer(client, config) {
|
|
|
1535
1740
|
max_pages: args.max_pages,
|
|
1536
1741
|
max_results: args.max_results,
|
|
1537
1742
|
sort_by: "best_hunt_value",
|
|
1538
|
-
include_target_samples:
|
|
1539
|
-
target_sample_programs:
|
|
1540
|
-
target_sample_size:
|
|
1743
|
+
include_target_samples: args.include_target_samples,
|
|
1744
|
+
target_sample_programs: args.target_sample_programs,
|
|
1745
|
+
target_sample_size: args.target_sample_size,
|
|
1541
1746
|
only_in_scope_targets: true,
|
|
1542
1747
|
only_bounty_eligible_targets: true,
|
|
1543
|
-
target_samples_only_wildcards:
|
|
1748
|
+
target_samples_only_wildcards: args.min_wildcards > 0,
|
|
1749
|
+
output_mode: args.output_mode,
|
|
1544
1750
|
upstream_request_budget: args.upstream_request_budget
|
|
1545
1751
|
})));
|
|
1546
|
-
|
|
1752
|
+
registerTool("get_program_delta", {
|
|
1547
1753
|
title: "Get Program Delta",
|
|
1548
1754
|
description: "Recent target changes for one program.",
|
|
1549
1755
|
inputSchema: {
|
|
@@ -1551,7 +1757,7 @@ export function createBbradarServer(client, config) {
|
|
|
1551
1757
|
include_removed: z.boolean().default(true),
|
|
1552
1758
|
include_ineligible: z.boolean().default(false),
|
|
1553
1759
|
include_out_of_scope: z.boolean().default(false),
|
|
1554
|
-
page_size: recentChangesPageSizeSchema.default(50)
|
|
1760
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(50, maxTargetChangesPageSize))
|
|
1555
1761
|
},
|
|
1556
1762
|
outputSchema: {
|
|
1557
1763
|
...apiEnvelopeOutputShape(),
|
|
@@ -1561,18 +1767,18 @@ export function createBbradarServer(client, config) {
|
|
|
1561
1767
|
},
|
|
1562
1768
|
annotations: readOnlyAnnotations
|
|
1563
1769
|
}, (args) => runTool("get_program_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramDelta(client, config, { ...args, program_id: programId }))));
|
|
1564
|
-
|
|
1770
|
+
registerTool("export_targets", {
|
|
1565
1771
|
title: "Export BBRadar Targets",
|
|
1566
|
-
description: `Read-only target export. limit max ${
|
|
1772
|
+
description: `Read-only target export. limit max ${maxTargetExport}.`,
|
|
1567
1773
|
inputSchema: {
|
|
1568
|
-
program_ids: z.array(programIdentifierSchema).max(
|
|
1774
|
+
program_ids: z.array(programIdentifierSchema).max(maxTargetExportProgramIds).default([]),
|
|
1569
1775
|
platforms: platformListSchema(),
|
|
1570
1776
|
tags: tagListSchema(),
|
|
1571
1777
|
opportunity_levels: z.array(z.enum(["elite", "hot", "strong", "potential"])).max(4).default([]),
|
|
1572
1778
|
include_out_of_scope: z.boolean().default(false),
|
|
1573
1779
|
include_ineligible: z.boolean().default(false),
|
|
1574
1780
|
format: z.enum(["json", "csv"]).default("json"),
|
|
1575
|
-
limit: z.number().int().min(1).max(
|
|
1781
|
+
limit: z.number().int().min(1).max(maxTargetExport).default(maxTargetExport)
|
|
1576
1782
|
},
|
|
1577
1783
|
outputSchema: {
|
|
1578
1784
|
...apiEnvelopeOutputShape(),
|
|
@@ -1618,11 +1824,14 @@ export function createBbradarServer(client, config) {
|
|
|
1618
1824
|
});
|
|
1619
1825
|
}
|
|
1620
1826
|
}));
|
|
1621
|
-
registerPrompts(server);
|
|
1622
|
-
registerResources(server, client, config, exportStore);
|
|
1827
|
+
registerPrompts(server, config.toolProfile);
|
|
1828
|
+
registerResources(server, client, config, exportStore, continuationStore);
|
|
1623
1829
|
return server;
|
|
1624
1830
|
}
|
|
1625
|
-
function bbradarGuide(topic) {
|
|
1831
|
+
function bbradarGuide(topic, profile, client) {
|
|
1832
|
+
if (profile === "core") {
|
|
1833
|
+
return coreBbradarGuide(topic, client);
|
|
1834
|
+
}
|
|
1626
1835
|
const primaryEntryPoints = [
|
|
1627
1836
|
{
|
|
1628
1837
|
tool: "find_program_candidates",
|
|
@@ -1654,6 +1863,11 @@ function bbradarGuide(topic) {
|
|
|
1654
1863
|
first_tool: "find_program_candidates",
|
|
1655
1864
|
then: "Use get_program_brief on the best program_id if the user wants details."
|
|
1656
1865
|
},
|
|
1866
|
+
{
|
|
1867
|
+
user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
|
|
1868
|
+
first_tool: "list_filters",
|
|
1869
|
+
then: "Resolve the excluded platform names, then call find_programs with exclude_platforms, exclude_web3=true, exclude_non_web=true, reward/scope/freshness thresholds, and sort_by=most_new_targets. If strict local filters return zero, relax one threshold and retry; cached source pages make the retry cheap."
|
|
1870
|
+
},
|
|
1657
1871
|
{
|
|
1658
1872
|
user_goal: "named program summary",
|
|
1659
1873
|
first_tool: "run_program_name_action",
|
|
@@ -1674,6 +1888,43 @@ function bbradarGuide(topic) {
|
|
|
1674
1888
|
first_tool: "find_recently_updated_programs",
|
|
1675
1889
|
then: "Use get_latest_added_targets for global added targets, or check_program_new_targets when the user names one program."
|
|
1676
1890
|
},
|
|
1891
|
+
{
|
|
1892
|
+
user_goal: "repository changes or repository polling",
|
|
1893
|
+
first_tool: "get_repository_changes",
|
|
1894
|
+
arguments: { include_targets: false, max_cursor_pages: 2 },
|
|
1895
|
+
then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
|
|
1896
|
+
},
|
|
1897
|
+
{
|
|
1898
|
+
user_goal: "rewarded Web3 targets with recent repository changes on named platforms",
|
|
1899
|
+
first_tool: "get_repository_changes",
|
|
1900
|
+
arguments: { platforms: ["requested platforms"], include_targets: true, output_mode: "full", page_size: 100 },
|
|
1901
|
+
parallel_with: {
|
|
1902
|
+
tool: "find_programs",
|
|
1903
|
+
arguments: { platforms: ["requested platforms"], web3: true, require_bounty: true, min_bounty_max: 5000, sort_by: "highest_reward", include_target_samples: false }
|
|
1904
|
+
},
|
|
1905
|
+
then: "Intersect repository-event program ids with ranked program ids. Targets belong to the repository event association even when target rows omit program_id; keep strong target scores/labels and only the requested program platforms."
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
user_goal: "latest added wildcard targets with platform, reward, or report-count filters",
|
|
1909
|
+
first_tool: "get_latest_added_targets",
|
|
1910
|
+
arguments: {
|
|
1911
|
+
target_type: "wildcard",
|
|
1912
|
+
platforms: ["requested platform"],
|
|
1913
|
+
min_reward: 5000,
|
|
1914
|
+
max_public_report_count: 10,
|
|
1915
|
+
include_full_target_list: false
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
user_goal: "wildcard programs with low report count and good rewards",
|
|
1920
|
+
first_tool: "find_wildcard_programs",
|
|
1921
|
+
arguments: {
|
|
1922
|
+
platforms: ["requested platform"],
|
|
1923
|
+
min_reward: 5000,
|
|
1924
|
+
max_public_report_count: 10,
|
|
1925
|
+
include_target_samples: true
|
|
1926
|
+
}
|
|
1927
|
+
},
|
|
1677
1928
|
{
|
|
1678
1929
|
user_goal: "compare a short list",
|
|
1679
1930
|
first_tool: "compare_programs_compact",
|
|
@@ -1691,17 +1942,19 @@ function bbradarGuide(topic) {
|
|
|
1691
1942
|
}
|
|
1692
1943
|
];
|
|
1693
1944
|
const advancedTools = {
|
|
1694
|
-
discovery: ["find_stack_matches", "find_language_programs", "find_target_type_programs", "find_reward_programs", "find_paid_programs", "find_web3_contests", "find_low_noise_programs"],
|
|
1695
|
-
freshness: ["find_recently_updated_programs", "find_fresh_hunt_candidates", "get_latest_added_targets", "get_recent_target_activity", "get_program_scope_delta", "check_watchlist_new_targets"],
|
|
1945
|
+
discovery: ["find_wildcard_programs", "find_low_competition_high_reward", "find_stack_matches", "find_language_programs", "find_target_type_programs", "find_reward_programs", "find_paid_programs", "find_web3_contests", "find_low_noise_programs"],
|
|
1946
|
+
freshness: ["find_recently_added_wildcards", "find_recently_updated_programs", "find_fresh_hunt_candidates", "get_latest_added_targets", "get_recent_target_activity", "get_program_scope_delta", "check_watchlist_new_targets"],
|
|
1696
1947
|
details: ["get_program_brief", "get_program_scope_summary", "get_program_target_breakdown", "find_programs_by_target"],
|
|
1697
1948
|
exports: ["export_targets"]
|
|
1698
1949
|
};
|
|
1699
1950
|
const filters = {
|
|
1700
1951
|
platforms: "Only set when the user explicitly names a platform. Do not infer Immunefi, HackerOne, etc.",
|
|
1952
|
+
exclude_platforms: "Use exact observed values from list_filters for user-requested platform or geographic exclusions.",
|
|
1701
1953
|
tags: "Broad combined tag filter across scope, language, and target-type signals.",
|
|
1702
1954
|
scope_tags: "Scope category signals such as domain, api, mobile, smart-contract, wildcard.",
|
|
1703
1955
|
target_types: "Target-kind signals such as domain, api, url, mobile, smart-contract.",
|
|
1704
1956
|
language_tags: "Technology/language signals such as javascript, solidity, web3, php, python.",
|
|
1957
|
+
exclude_web3: "Set true for Web2-only requests.",
|
|
1705
1958
|
vdp_mode: "Default exclude. Include VDP/no-bounty data only when asked."
|
|
1706
1959
|
};
|
|
1707
1960
|
const examples = [
|
|
@@ -1724,6 +1977,20 @@ function bbradarGuide(topic) {
|
|
|
1724
1977
|
{
|
|
1725
1978
|
user_says: "who owns https://api.example.com/v1/users",
|
|
1726
1979
|
call: { tool: "find_programs_by_target", arguments: { target_query: "https://api.example.com/v1/users" } }
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
user_says: "latest HackerOne wildcard targets with rewards over 5000",
|
|
1983
|
+
call: {
|
|
1984
|
+
tool: "get_latest_added_targets",
|
|
1985
|
+
arguments: { target_type: "wildcard", platforms: ["HackerOne"], min_reward: 5000, include_full_target_list: false }
|
|
1986
|
+
}
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
user_says: "wildcard programs with under 10 reports and good rewards",
|
|
1990
|
+
call: {
|
|
1991
|
+
tool: "find_wildcard_programs",
|
|
1992
|
+
arguments: { max_public_report_count: 10, min_reward: 5000, include_target_samples: true }
|
|
1993
|
+
}
|
|
1727
1994
|
}
|
|
1728
1995
|
];
|
|
1729
1996
|
return {
|
|
@@ -1738,12 +2005,99 @@ function bbradarGuide(topic) {
|
|
|
1738
2005
|
require_bounty: true
|
|
1739
2006
|
},
|
|
1740
2007
|
primary_entry_points: topic === "start" ? primaryEntryPoints : primaryEntryPoints.filter((entry) => entry.tool !== "get_bbradar_guide"),
|
|
1741
|
-
decision_tree: decisionTree.filter((entry) =>
|
|
2008
|
+
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2009
|
+
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
1742
2010
|
filters,
|
|
1743
2011
|
advanced_tools: advancedTools,
|
|
1744
2012
|
examples
|
|
1745
2013
|
};
|
|
1746
2014
|
}
|
|
2015
|
+
function coreBbradarGuide(topic, client) {
|
|
2016
|
+
const decisionTree = [
|
|
2017
|
+
{
|
|
2018
|
+
user_goal: "find programs to hunt",
|
|
2019
|
+
first_tool: "find_programs",
|
|
2020
|
+
arguments: { max_results: 10, output_mode: "compact" }
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
|
|
2024
|
+
first_tool: "list_filters",
|
|
2025
|
+
then: "Resolve excluded platform names, then call find_programs with exclude_platforms, exclude_web3=true, exclude_non_web=true, reward/scope/freshness thresholds, and sort_by=most_new_targets. Relax one local threshold on an empty result; cached source pages make retries fast."
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
user_goal: "named program summary or action",
|
|
2029
|
+
first_tool: "run_program_name_action",
|
|
2030
|
+
arguments: { query: "program name", action: "brief" }
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
user_goal: "named program targets",
|
|
2034
|
+
first_tool: "get_program_eligible_targets_by_name",
|
|
2035
|
+
arguments: { query: "program name", target_list_mode: "identifiers" }
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
user_goal: "find which program owns a target",
|
|
2039
|
+
first_tool: "find_programs_by_target",
|
|
2040
|
+
arguments: { target_query: "domain, URL, wildcard, package/repo name, or short target phrase", match_mode: "smart" }
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
user_goal: "fresh or recently changed scope",
|
|
2044
|
+
first_tool: "get_recent_changes",
|
|
2045
|
+
then: "Use get_latest_added_targets when only newly added targets are needed."
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
user_goal: "repository changes or repository polling",
|
|
2049
|
+
first_tool: "get_repository_changes",
|
|
2050
|
+
arguments: { include_targets: false, max_cursor_pages: 2 }
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
user_goal: "rewarded Web3 targets with recent repository changes on named platforms",
|
|
2054
|
+
first_tool: "get_repository_changes",
|
|
2055
|
+
arguments: { platforms: ["requested platforms"], include_targets: true, output_mode: "full", page_size: 100 },
|
|
2056
|
+
parallel_with: {
|
|
2057
|
+
tool: "find_programs",
|
|
2058
|
+
arguments: { platforms: ["requested platforms"], web3: true, require_bounty: true, min_bounty_max: 5000, sort_by: "highest_reward", include_target_samples: false }
|
|
2059
|
+
},
|
|
2060
|
+
then: "Intersect event program ids with ranked program ids. Treat targets as event-level associations when target rows omit program_id, keep strong target scores/labels, and discard non-requested associated platforms."
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
user_goal: "export targets",
|
|
2064
|
+
first_tool: "export_targets",
|
|
2065
|
+
then: "Use the returned bbradar://exports resource_uri for the full local export payload."
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
user_goal: "unknown filter values",
|
|
2069
|
+
first_tool: "list_filters",
|
|
2070
|
+
then: "Reuse observed platforms, tags, target types, and language tags in find_programs."
|
|
2071
|
+
}
|
|
2072
|
+
];
|
|
2073
|
+
return {
|
|
2074
|
+
topic,
|
|
2075
|
+
tool_profile: "core",
|
|
2076
|
+
rule_zero: "BBRadar is passive intelligence only. Never scan or contact targets.",
|
|
2077
|
+
defaults: {
|
|
2078
|
+
output_mode: "compact",
|
|
2079
|
+
target_list_mode: "identifiers",
|
|
2080
|
+
target_match_mode: "smart",
|
|
2081
|
+
include_out_of_scope: false,
|
|
2082
|
+
include_ineligible: false,
|
|
2083
|
+
require_bounty: true
|
|
2084
|
+
},
|
|
2085
|
+
primary_entry_points: [
|
|
2086
|
+
"find_programs",
|
|
2087
|
+
"run_program_name_action",
|
|
2088
|
+
"get_program_eligible_targets_by_name",
|
|
2089
|
+
"find_programs_by_target",
|
|
2090
|
+
"get_bbradar_guide"
|
|
2091
|
+
],
|
|
2092
|
+
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2093
|
+
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
2094
|
+
available_tools: [...CORE_TOOL_NAMES].filter((name) => toolAvailableForContract(client, name))
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
function toolAvailableForContract(client, toolName) {
|
|
2098
|
+
const endpoint = TOOL_ENDPOINT_REQUIREMENTS[toolName];
|
|
2099
|
+
return endpoint === undefined || client.supportsEndpoint(endpoint);
|
|
2100
|
+
}
|
|
1747
2101
|
function guideEntryMatchesTopic(topic, entry) {
|
|
1748
2102
|
const goal = typeof entry.user_goal === "string" ? entry.user_goal : "";
|
|
1749
2103
|
const firstTool = typeof entry.first_tool === "string" ? entry.first_tool : "";
|
|
@@ -1770,15 +2124,52 @@ function guideEntryMatchesTopic(topic, entry) {
|
|
|
1770
2124
|
}
|
|
1771
2125
|
return true;
|
|
1772
2126
|
}
|
|
1773
|
-
function registerResources(server, client, config, exportStore) {
|
|
2127
|
+
function registerResources(server, client, config, exportStore, continuationStore) {
|
|
2128
|
+
server.registerResource("continuation", new ResourceTemplate("bbradar://continuations{?continuation_id,offset,limit}", { list: undefined }), {
|
|
2129
|
+
title: "BBRadar Result Continuation",
|
|
2130
|
+
description: "Bounded, short-lived in-memory continuation page for a large tool result.",
|
|
2131
|
+
mimeType: "application/json"
|
|
2132
|
+
}, async (uri) => {
|
|
2133
|
+
const continuationId = uri.searchParams.get("continuation_id") ?? "";
|
|
2134
|
+
pruneExpiredContinuations(continuationStore);
|
|
2135
|
+
const stored = continuationStore.get(continuationId);
|
|
2136
|
+
if (!stored) {
|
|
2137
|
+
return jsonResource(uri.toString(), {
|
|
2138
|
+
request_id: randomUUID(),
|
|
2139
|
+
error: { message: "Continuation not found or expired from local memory." }
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
const offset = boundedResourceInteger(uri.searchParams.get("offset"), 0, 0, stored.items.length);
|
|
2143
|
+
const limit = boundedResourceInteger(uri.searchParams.get("limit"), config.continuationInlineLimit ?? 50, 1, 100);
|
|
2144
|
+
const items = stored.items.slice(offset, offset + limit);
|
|
2145
|
+
const nextOffset = offset + items.length;
|
|
2146
|
+
const hasMore = nextOffset < stored.items.length;
|
|
2147
|
+
return jsonResource(uri.toString(), {
|
|
2148
|
+
request_id: randomUUID(),
|
|
2149
|
+
continuation_id: continuationId,
|
|
2150
|
+
label: stored.label,
|
|
2151
|
+
items,
|
|
2152
|
+
meta: {
|
|
2153
|
+
offset,
|
|
2154
|
+
limit,
|
|
2155
|
+
returned: items.length,
|
|
2156
|
+
total: stored.items.length,
|
|
2157
|
+
has_more: hasMore,
|
|
2158
|
+
next_resource_uri: hasMore
|
|
2159
|
+
? continuationResourceUri(continuationId, nextOffset, limit)
|
|
2160
|
+
: undefined
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
1774
2164
|
server.registerResource("target-export", new ResourceTemplate("bbradar://exports{?export_id}", { list: undefined }), {
|
|
1775
2165
|
title: "BBRadar Target Export",
|
|
1776
|
-
description: "
|
|
2166
|
+
description: "Bounded, short-lived in-memory target export payload by export_id query parameter.",
|
|
1777
2167
|
mimeType: "application/json"
|
|
1778
2168
|
}, async (uri) => {
|
|
1779
2169
|
const exportId = uri.searchParams.get("export_id") ?? "";
|
|
1780
|
-
|
|
1781
|
-
|
|
2170
|
+
pruneExpiredExports(exportStore);
|
|
2171
|
+
const storedExport = exportStore.get(exportId);
|
|
2172
|
+
if (!storedExport) {
|
|
1782
2173
|
return jsonResource(uri.toString(), {
|
|
1783
2174
|
request_id: randomUUID(),
|
|
1784
2175
|
error: {
|
|
@@ -1789,7 +2180,7 @@ function registerResources(server, client, config, exportStore) {
|
|
|
1789
2180
|
return jsonResource(uri.toString(), {
|
|
1790
2181
|
request_id: randomUUID(),
|
|
1791
2182
|
export_id: exportId,
|
|
1792
|
-
export: payload
|
|
2183
|
+
export: storedExport.payload
|
|
1793
2184
|
});
|
|
1794
2185
|
});
|
|
1795
2186
|
server.registerResource("program", new ResourceTemplate("bbradar://program{?program_id}", { list: undefined }), {
|
|
@@ -3178,13 +3569,26 @@ async function findProgramsByTarget(client, config, input) {
|
|
|
3178
3569
|
};
|
|
3179
3570
|
const sourceRequests = [];
|
|
3180
3571
|
const matches = [];
|
|
3572
|
+
const directHandleFirst = !queryLooksLikeTargetIdentifier(input.target_query) &&
|
|
3573
|
+
!/\s/.test(input.target_query.trim()) &&
|
|
3574
|
+
looksLikeDirectProgramHandleQuery(input.target_query);
|
|
3575
|
+
if (input.use_api_search && !directHandleFirst) {
|
|
3576
|
+
const directResult = await searchProgramsByTargetApi(client, config, input, budget, warnings, sourceRequests);
|
|
3577
|
+
if (directResult) {
|
|
3578
|
+
return directResult;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3181
3581
|
const directHandles = await collectDirectHandlePrograms(client, {
|
|
3182
3582
|
query: input.target_query,
|
|
3183
3583
|
platforms: input.platforms,
|
|
3184
3584
|
budget,
|
|
3185
3585
|
warnings,
|
|
3186
3586
|
source: "target_query_program_id_guess",
|
|
3187
|
-
maxProgramIds:
|
|
3587
|
+
maxProgramIds: input.use_api_search && directHandleFirst
|
|
3588
|
+
? 4
|
|
3589
|
+
: queryLooksLikeTargetIdentifier(input.target_query) || looksLikeDirectProgramHandleQuery(input.target_query)
|
|
3590
|
+
? MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS
|
|
3591
|
+
: 4,
|
|
3188
3592
|
maxHandleVariants: 1,
|
|
3189
3593
|
stopOnFirstMatch: true
|
|
3190
3594
|
});
|
|
@@ -3204,8 +3608,8 @@ async function findProgramsByTarget(client, config, input) {
|
|
|
3204
3608
|
}
|
|
3205
3609
|
});
|
|
3206
3610
|
}
|
|
3207
|
-
if (input.use_api_search) {
|
|
3208
|
-
const directResult = await searchProgramsByTargetApi(client, config, input, warnings);
|
|
3611
|
+
if (input.use_api_search && directHandleFirst) {
|
|
3612
|
+
const directResult = await searchProgramsByTargetApi(client, config, input, budget, warnings, sourceRequests);
|
|
3209
3613
|
if (directResult) {
|
|
3210
3614
|
return directResult;
|
|
3211
3615
|
}
|
|
@@ -3310,13 +3714,22 @@ function programsByTargetResult(input, warnings, sourceRequests, matches, budget
|
|
|
3310
3714
|
meta: options.meta
|
|
3311
3715
|
});
|
|
3312
3716
|
}
|
|
3313
|
-
async function searchProgramsByTargetApi(client, config, input, warnings) {
|
|
3717
|
+
async function searchProgramsByTargetApi(client, config, input, budget, warnings, sourceRequests) {
|
|
3718
|
+
if (!client.supportsEndpoint("target_search")) {
|
|
3719
|
+
warnings.push("The negotiated API contract does not expose direct target search; using sampled program target lists.");
|
|
3720
|
+
return undefined;
|
|
3721
|
+
}
|
|
3314
3722
|
const searchQueries = targetApiSearchQueries(input.target_query);
|
|
3315
3723
|
const grouped = new Map();
|
|
3316
|
-
const sourceRequests = [];
|
|
3317
3724
|
let rawMatchesScanned = 0;
|
|
3725
|
+
let searchQueriesChecked = 0;
|
|
3318
3726
|
let lastMeta;
|
|
3319
3727
|
for (const searchQuery of searchQueries) {
|
|
3728
|
+
if (!tryConsumeBudget(budget)) {
|
|
3729
|
+
addUniqueWarning(warnings, "Upstream request budget was exhausted before all target search variants could be checked.");
|
|
3730
|
+
break;
|
|
3731
|
+
}
|
|
3732
|
+
searchQueriesChecked += 1;
|
|
3320
3733
|
let api;
|
|
3321
3734
|
try {
|
|
3322
3735
|
api = await client.searchTargets({
|
|
@@ -3327,10 +3740,12 @@ async function searchProgramsByTargetApi(client, config, input, warnings) {
|
|
|
3327
3740
|
include_out_of_scope: input.include_out_of_scope,
|
|
3328
3741
|
include_ineligible: input.include_ineligible,
|
|
3329
3742
|
page: 1,
|
|
3330
|
-
page_size: Math.max(input.max_results * input.max_targets_per_program, input.max_results)
|
|
3743
|
+
page_size: Math.min(client.endpointNumber("target_search", "max_page_size", MAX_TARGETS_PER_PROGRAM), Math.max(input.max_results * input.max_targets_per_program, input.max_results))
|
|
3331
3744
|
});
|
|
3745
|
+
refundBudgetIfNoUpstreamRequest(budget, api);
|
|
3332
3746
|
}
|
|
3333
3747
|
catch (error) {
|
|
3748
|
+
refundBudgetIfNoUpstreamError(budget, error);
|
|
3334
3749
|
if (error instanceof BBRadarApiError && (error.status === 404 || error.status === 405)) {
|
|
3335
3750
|
warnings.push("Direct target search API is unavailable; fell back to sampled program target lists.");
|
|
3336
3751
|
return undefined;
|
|
@@ -3371,15 +3786,93 @@ async function searchProgramsByTargetApi(client, config, input, warnings) {
|
|
|
3371
3786
|
if (!targetMatchesQuery(target, input.target_query, input.match_mode)) {
|
|
3372
3787
|
continue;
|
|
3373
3788
|
}
|
|
3374
|
-
const existing = grouped.get(programId) ?? {
|
|
3375
|
-
|
|
3789
|
+
const existing = grouped.get(programId) ?? {
|
|
3790
|
+
program: addProgramResourceLinks({ ...program, id: programId }),
|
|
3791
|
+
targets: [],
|
|
3792
|
+
targetKeys: new Set()
|
|
3793
|
+
};
|
|
3794
|
+
const targetKey = stableStringify({
|
|
3795
|
+
identifier: targetIdentifier(target),
|
|
3796
|
+
target_type: stringField(target, "target_type")
|
|
3797
|
+
});
|
|
3798
|
+
if (!existing.targetKeys.has(targetKey)) {
|
|
3799
|
+
existing.targetKeys.add(targetKey);
|
|
3800
|
+
existing.targets.push(target);
|
|
3801
|
+
}
|
|
3376
3802
|
grouped.set(programId, existing);
|
|
3377
3803
|
}
|
|
3378
3804
|
}
|
|
3379
|
-
if (grouped.size >= input.max_results
|
|
3805
|
+
if (grouped.size >= input.max_results ||
|
|
3806
|
+
(grouped.size > 0 && input.match_mode === "smart")) {
|
|
3380
3807
|
break;
|
|
3381
3808
|
}
|
|
3382
3809
|
}
|
|
3810
|
+
if (grouped.size > 0 && input.match_mode === "smart") {
|
|
3811
|
+
await mapWithConcurrency([...grouped.entries()].slice(0, input.max_results), TARGET_SAMPLE_CONCURRENCY, async ([programId, entry]) => {
|
|
3812
|
+
if (!tryConsumeBudget(budget)) {
|
|
3813
|
+
addUniqueWarning(warnings, "Upstream request budget was exhausted before matched program target lists could be hydrated.");
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
try {
|
|
3817
|
+
const targetsApi = await client.getProgramTargets(programId);
|
|
3818
|
+
refundBudgetIfNoUpstreamRequest(budget, targetsApi);
|
|
3819
|
+
sourceRequests.push({
|
|
3820
|
+
source: "program_targets",
|
|
3821
|
+
program_id: programId,
|
|
3822
|
+
request_id: targetsApi.requestId,
|
|
3823
|
+
upstream_request_id: targetsApi.upstreamRequestId,
|
|
3824
|
+
...apiSourceMetadata(targetsApi)
|
|
3825
|
+
});
|
|
3826
|
+
const targetsData = readObject(targetsApi.data);
|
|
3827
|
+
const hydratedTargets = readArray(targetsData?.targets).map(sanitizeTarget);
|
|
3828
|
+
const hydratedTargetOrder = new Map(hydratedTargets.map((target, index) => [
|
|
3829
|
+
stableStringify({
|
|
3830
|
+
identifier: targetIdentifier(target),
|
|
3831
|
+
target_type: stringField(target, "target_type")
|
|
3832
|
+
}),
|
|
3833
|
+
index
|
|
3834
|
+
]));
|
|
3835
|
+
for (const target of hydratedTargets) {
|
|
3836
|
+
if (!targetIdentifier(target) ||
|
|
3837
|
+
!targetHasAllowedScope(target, {
|
|
3838
|
+
include_out_of_scope: input.include_out_of_scope,
|
|
3839
|
+
include_ineligible: input.include_ineligible,
|
|
3840
|
+
strict_scope_filter: false
|
|
3841
|
+
}) ||
|
|
3842
|
+
!targetMatchesQuery(target, input.target_query, input.match_mode)) {
|
|
3843
|
+
continue;
|
|
3844
|
+
}
|
|
3845
|
+
const targetKey = stableStringify({
|
|
3846
|
+
identifier: targetIdentifier(target),
|
|
3847
|
+
target_type: stringField(target, "target_type")
|
|
3848
|
+
});
|
|
3849
|
+
if (!entry.targetKeys.has(targetKey)) {
|
|
3850
|
+
entry.targetKeys.add(targetKey);
|
|
3851
|
+
entry.targets.push(target);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
entry.targets.sort((left, right) => {
|
|
3855
|
+
const leftKey = stableStringify({
|
|
3856
|
+
identifier: targetIdentifier(left),
|
|
3857
|
+
target_type: stringField(left, "target_type")
|
|
3858
|
+
});
|
|
3859
|
+
const rightKey = stableStringify({
|
|
3860
|
+
identifier: targetIdentifier(right),
|
|
3861
|
+
target_type: stringField(right, "target_type")
|
|
3862
|
+
});
|
|
3863
|
+
return ((hydratedTargetOrder.get(leftKey) ?? Number.MAX_SAFE_INTEGER) -
|
|
3864
|
+
(hydratedTargetOrder.get(rightKey) ?? Number.MAX_SAFE_INTEGER));
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
catch (error) {
|
|
3868
|
+
refundBudgetIfNoUpstreamError(budget, error);
|
|
3869
|
+
if (!isProgramNotFoundError(error)) {
|
|
3870
|
+
throw error;
|
|
3871
|
+
}
|
|
3872
|
+
addUniqueWarning(warnings, `Could not hydrate target matches for ${programId}; returned direct target-search matches only.`);
|
|
3873
|
+
}
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3383
3876
|
const matches = [...grouped.values()].slice(0, input.max_results).map((entry) => stripUndefined({
|
|
3384
3877
|
program: formatProgram(entry.program, "compact"),
|
|
3385
3878
|
match_count: entry.targets.length,
|
|
@@ -3407,6 +3900,14 @@ async function searchProgramsByTargetApi(client, config, input, warnings) {
|
|
|
3407
3900
|
},
|
|
3408
3901
|
matches,
|
|
3409
3902
|
programs: matches.map((match) => readObject(match.program)).filter((program) => program !== undefined),
|
|
3903
|
+
upstream_budget: {
|
|
3904
|
+
used: budget.initial - budget.remaining,
|
|
3905
|
+
remaining: budget.remaining
|
|
3906
|
+
},
|
|
3907
|
+
ranking_scope: {
|
|
3908
|
+
mode: "target_search_api",
|
|
3909
|
+
possibly_incomplete: grouped.size < input.max_results && searchQueriesChecked < searchQueries.length
|
|
3910
|
+
},
|
|
3410
3911
|
meta: stripUndefined({
|
|
3411
3912
|
...(lastMeta ?? {}),
|
|
3412
3913
|
returned: matches.length,
|
|
@@ -3503,7 +4004,8 @@ async function listFilters(client, input) {
|
|
|
3503
4004
|
}
|
|
3504
4005
|
}
|
|
3505
4006
|
}
|
|
3506
|
-
catch {
|
|
4007
|
+
catch (error) {
|
|
4008
|
+
refundBudgetIfNoUpstreamError(budget, error);
|
|
3507
4009
|
// Facets are best-effort; list_filters should still return observed program filters.
|
|
3508
4010
|
}
|
|
3509
4011
|
if (input.include_target_facets && input.target_sample_programs > 0) {
|
|
@@ -3639,11 +4141,15 @@ async function comparePrograms(client, config, input) {
|
|
|
3639
4141
|
});
|
|
3640
4142
|
}
|
|
3641
4143
|
async function summarizeProgramActivity(client, config, programId, recentChangesLimit) {
|
|
3642
|
-
const programApi = await
|
|
4144
|
+
const [programApi, changes] = await Promise.all([
|
|
4145
|
+
client.getProgram(programId),
|
|
4146
|
+
recentChangesLimit > 0
|
|
4147
|
+
? fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true)
|
|
4148
|
+
: Promise.resolve(undefined)
|
|
4149
|
+
]);
|
|
3643
4150
|
const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
|
|
3644
4151
|
const scopeSummary = readObject(program.scope_summary);
|
|
3645
4152
|
const targetActivity = readObject(scopeSummary?.target_activity);
|
|
3646
|
-
const changes = recentChangesLimit > 0 ? await fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true) : undefined;
|
|
3647
4153
|
return {
|
|
3648
4154
|
request_id: randomUUID(),
|
|
3649
4155
|
source_requests: [
|
|
@@ -3670,12 +4176,14 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
3670
4176
|
include_removed: false,
|
|
3671
4177
|
include_ineligible: input.include_ineligible,
|
|
3672
4178
|
include_out_of_scope: input.include_out_of_scope,
|
|
4179
|
+
platforms: input.platforms,
|
|
3673
4180
|
tags: [input.target_type],
|
|
3674
4181
|
page: 1,
|
|
3675
4182
|
page_size: input.recent_changes_page_size
|
|
3676
4183
|
});
|
|
3677
4184
|
const recentData = readObject(recentApi.data);
|
|
3678
4185
|
const changes = readArray(recentData?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
|
|
4186
|
+
const platformFilters = new Set(input.platforms.map(normalizeTag));
|
|
3679
4187
|
const matchingChanges = changes
|
|
3680
4188
|
.filter((change) => {
|
|
3681
4189
|
const target = readObject(change.target);
|
|
@@ -3684,6 +4192,7 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
3684
4192
|
programId !== undefined &&
|
|
3685
4193
|
target !== undefined &&
|
|
3686
4194
|
targetMatchesRequestedType(target, input.target_type) &&
|
|
4195
|
+
latestAddedProgramMatches(change, input, platformFilters) &&
|
|
3687
4196
|
targetHasAllowedScope(target, {
|
|
3688
4197
|
include_out_of_scope: input.include_out_of_scope,
|
|
3689
4198
|
include_ineligible: input.include_ineligible,
|
|
@@ -3691,6 +4200,11 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
3691
4200
|
}));
|
|
3692
4201
|
})
|
|
3693
4202
|
.sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
|
|
4203
|
+
const limitedMatches = matchingChanges.slice(0, input.max_results);
|
|
4204
|
+
const matches = limitedMatches.map((change, index) => ({
|
|
4205
|
+
rank: index + 1,
|
|
4206
|
+
...formatChange(change, "compact")
|
|
4207
|
+
}));
|
|
3694
4208
|
const latest = matchingChanges[0];
|
|
3695
4209
|
const sourceRequests = [
|
|
3696
4210
|
{
|
|
@@ -3705,9 +4219,12 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
3705
4219
|
request_id: randomUUID(),
|
|
3706
4220
|
source_requests: sourceRequests,
|
|
3707
4221
|
query: latestAddedTargetsQuery(input),
|
|
4222
|
+
matches: [],
|
|
3708
4223
|
new_targets: [],
|
|
3709
4224
|
meta: {
|
|
3710
4225
|
recent_changes_scanned: changes.length,
|
|
4226
|
+
matching_changes_scanned: 0,
|
|
4227
|
+
matches_returned: 0,
|
|
3711
4228
|
no_match: true
|
|
3712
4229
|
}
|
|
3713
4230
|
};
|
|
@@ -3750,11 +4267,14 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
3750
4267
|
query: latestAddedTargetsQuery(input),
|
|
3751
4268
|
latest_added_at: latestAddedAt,
|
|
3752
4269
|
program,
|
|
4270
|
+
matches,
|
|
3753
4271
|
new_targets: formatTargetList(latestNewTargets, input.full_target_list_mode),
|
|
3754
4272
|
full_targets: input.include_full_target_list ? formatTargetList(limitedFullTargets, input.full_target_list_mode) : undefined,
|
|
3755
4273
|
meta: stripUndefined({
|
|
3756
4274
|
recent_changes_scanned: changes.length,
|
|
3757
4275
|
matching_changes_scanned: matchingChanges.length,
|
|
4276
|
+
matches_returned: matches.length,
|
|
4277
|
+
matches_has_more: matches.length < matchingChanges.length,
|
|
3758
4278
|
new_targets_returned: formatTargetList(latestNewTargets, input.full_target_list_mode).length,
|
|
3759
4279
|
full_targets_total_active: totalActiveTargets,
|
|
3760
4280
|
full_targets_total_after_filters: totalAfterFilters,
|
|
@@ -3763,9 +4283,52 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
3763
4283
|
})
|
|
3764
4284
|
});
|
|
3765
4285
|
}
|
|
4286
|
+
function latestAddedProgramMatches(change, input, platformFilters) {
|
|
4287
|
+
const rawProgram = readObject(change.program);
|
|
4288
|
+
if (!rawProgram) {
|
|
4289
|
+
return false;
|
|
4290
|
+
}
|
|
4291
|
+
const program = addProgramResourceLinks(rawProgram);
|
|
4292
|
+
const rewardRange = readObject(program.reward_range);
|
|
4293
|
+
const platform = normalizeTag(stringField(program, "platform") ?? "");
|
|
4294
|
+
const bountyMin = numberField(rewardRange, "min");
|
|
4295
|
+
const bountyMax = numberField(rewardRange, "max");
|
|
4296
|
+
const publicReportCount = numberField(program, "public_report_count");
|
|
4297
|
+
if (platformFilters.size > 0 && !platformFilters.has(platform)) {
|
|
4298
|
+
return false;
|
|
4299
|
+
}
|
|
4300
|
+
if (input.min_reward !== undefined) {
|
|
4301
|
+
const reward = input.reward_threshold_mode === "min_at_least" ? bountyMin : bountyMax;
|
|
4302
|
+
if ((reward ?? 0) < input.min_reward) {
|
|
4303
|
+
return false;
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
if (input.require_known_report_count && publicReportCount === undefined) {
|
|
4307
|
+
return false;
|
|
4308
|
+
}
|
|
4309
|
+
if (input.max_public_report_count !== undefined) {
|
|
4310
|
+
if (publicReportCount === undefined) {
|
|
4311
|
+
return input.include_unknown_report_count;
|
|
4312
|
+
}
|
|
4313
|
+
if (publicReportCount > input.max_public_report_count) {
|
|
4314
|
+
return false;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
return true;
|
|
4318
|
+
}
|
|
3766
4319
|
async function getProgramTargetsPayload(client, input, programId) {
|
|
3767
|
-
const api = await client.getProgramTargets(programId
|
|
4320
|
+
const api = await client.getProgramTargets(programId, {
|
|
4321
|
+
search: input.search,
|
|
4322
|
+
target_type: input.target_type,
|
|
4323
|
+
eligibility: input.include_ineligible ? "all" : "eligible",
|
|
4324
|
+
minTargetScore: input.min_target_score,
|
|
4325
|
+
targetOpportunityLabel: input.target_opportunity_label,
|
|
4326
|
+
competitionRisk: input.competition_risk,
|
|
4327
|
+
page: 1,
|
|
4328
|
+
page_size: client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM)
|
|
4329
|
+
});
|
|
3768
4330
|
const data = readObject(api.data);
|
|
4331
|
+
const upstreamMeta = readObject(data?.meta);
|
|
3769
4332
|
const rawTargets = readArray(data?.targets);
|
|
3770
4333
|
const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, input));
|
|
3771
4334
|
const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
|
|
@@ -3776,10 +4339,13 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
3776
4339
|
offset: input.offset,
|
|
3777
4340
|
limit: input.limit,
|
|
3778
4341
|
output_mode: input.output_mode,
|
|
3779
|
-
total_active_targets: rawTargets.length,
|
|
4342
|
+
total_active_targets: readNumber(upstreamMeta?.total) ?? rawTargets.length,
|
|
3780
4343
|
total_after_filters: sanitizedTargets.length,
|
|
3781
4344
|
returned: targets.length,
|
|
3782
|
-
has_more: input.offset + input.limit < sanitizedTargets.length,
|
|
4345
|
+
has_more: input.offset + input.limit < sanitizedTargets.length || readBoolean(upstreamMeta?.has_more) === true,
|
|
4346
|
+
upstream_has_more: readBoolean(upstreamMeta?.has_more),
|
|
4347
|
+
possibly_incomplete: readBoolean(upstreamMeta?.has_more) === true,
|
|
4348
|
+
data_capabilities: readObject(upstreamMeta?.data_capabilities),
|
|
3783
4349
|
target_source: "program_targets"
|
|
3784
4350
|
}
|
|
3785
4351
|
});
|
|
@@ -4319,9 +4885,11 @@ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback,
|
|
|
4319
4885
|
});
|
|
4320
4886
|
}
|
|
4321
4887
|
async function getProgramScopeDelta(client, config, input) {
|
|
4322
|
-
const programApi = await
|
|
4888
|
+
const [programApi, changesPayload] = await Promise.all([
|
|
4889
|
+
client.getProgram(input.program_id),
|
|
4890
|
+
fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed || input.change_type === "removed", input.include_ineligible, input.include_out_of_scope, input.max_targets)
|
|
4891
|
+
]);
|
|
4323
4892
|
const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
|
|
4324
|
-
const changesPayload = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed || input.change_type === "removed", input.include_ineligible, input.include_out_of_scope, input.max_targets);
|
|
4325
4893
|
const filteredChanges = filterProgramScopeDeltaChanges(changesPayload.changes, input);
|
|
4326
4894
|
const limitedChanges = filteredChanges.slice(0, input.max_targets);
|
|
4327
4895
|
return stripUndefined({
|
|
@@ -4776,9 +5344,11 @@ async function getProgramBrief(client, config, input) {
|
|
|
4776
5344
|
});
|
|
4777
5345
|
}
|
|
4778
5346
|
async function getProgramDelta(client, config, input) {
|
|
4779
|
-
const programApi = await
|
|
5347
|
+
const [programApi, changes] = await Promise.all([
|
|
5348
|
+
client.getProgram(input.program_id),
|
|
5349
|
+
fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed, input.include_ineligible, input.include_out_of_scope)
|
|
5350
|
+
]);
|
|
4780
5351
|
const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
|
|
4781
|
-
const changes = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed, input.include_ineligible, input.include_out_of_scope);
|
|
4782
5352
|
return {
|
|
4783
5353
|
request_id: randomUUID(),
|
|
4784
5354
|
source_requests: [
|
|
@@ -4860,6 +5430,79 @@ function programTargetsResourceUri(programId) {
|
|
|
4860
5430
|
function exportResourceUri(exportId) {
|
|
4861
5431
|
return `bbradar://exports?export_id=${encodeURIComponent(exportId)}`;
|
|
4862
5432
|
}
|
|
5433
|
+
function continuationResourceUri(continuationId, offset, limit) {
|
|
5434
|
+
return `bbradar://continuations?continuation_id=${encodeURIComponent(continuationId)}&offset=${offset}&limit=${limit}`;
|
|
5435
|
+
}
|
|
5436
|
+
function applyCollectionContinuation(payload, field, label, store, config) {
|
|
5437
|
+
const items = Array.isArray(payload[field]) ? payload[field] : undefined;
|
|
5438
|
+
const inlineLimit = Math.min(100, Math.max(1, config.continuationInlineLimit ?? 50));
|
|
5439
|
+
if (!items || items.length <= inlineLimit) {
|
|
5440
|
+
return payload;
|
|
5441
|
+
}
|
|
5442
|
+
const continuationId = randomUUID();
|
|
5443
|
+
const expiresAt = Date.now() + (config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS);
|
|
5444
|
+
const stored = rememberContinuation(store, continuationId, label, items, expiresAt);
|
|
5445
|
+
if (!stored) {
|
|
5446
|
+
return {
|
|
5447
|
+
...payload,
|
|
5448
|
+
[field]: items.slice(0, inlineLimit),
|
|
5449
|
+
warnings: uniqueStrings([
|
|
5450
|
+
...readArray(payload.warnings).filter((entry) => typeof entry === "string"),
|
|
5451
|
+
"The complete result exceeded the local continuation-store byte limit; only the inline prefix is available."
|
|
5452
|
+
])
|
|
5453
|
+
};
|
|
5454
|
+
}
|
|
5455
|
+
return {
|
|
5456
|
+
...payload,
|
|
5457
|
+
[field]: items.slice(0, inlineLimit),
|
|
5458
|
+
continuation: {
|
|
5459
|
+
continuation_id: continuationId,
|
|
5460
|
+
resource_uri: continuationResourceUri(continuationId, inlineLimit, inlineLimit),
|
|
5461
|
+
collection: field,
|
|
5462
|
+
total: items.length,
|
|
5463
|
+
inline_returned: inlineLimit,
|
|
5464
|
+
remaining: items.length - inlineLimit,
|
|
5465
|
+
expires_at: new Date(expiresAt).toISOString()
|
|
5466
|
+
}
|
|
5467
|
+
};
|
|
5468
|
+
}
|
|
5469
|
+
function rememberContinuation(store, id, label, items, expiresAt) {
|
|
5470
|
+
const byteLength = Buffer.byteLength(JSON.stringify(items), "utf8");
|
|
5471
|
+
pruneExpiredContinuations(store);
|
|
5472
|
+
if (byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
5473
|
+
return false;
|
|
5474
|
+
}
|
|
5475
|
+
while (store.size >= MAX_LOCAL_EXPORT_RESOURCES ||
|
|
5476
|
+
retainedContinuationBytes(store) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
5477
|
+
const oldest = store.keys().next().value;
|
|
5478
|
+
if (oldest === undefined) {
|
|
5479
|
+
break;
|
|
5480
|
+
}
|
|
5481
|
+
store.delete(oldest);
|
|
5482
|
+
}
|
|
5483
|
+
store.set(id, { items, label, byteLength, expiresAt });
|
|
5484
|
+
return true;
|
|
5485
|
+
}
|
|
5486
|
+
function pruneExpiredContinuations(store, now = Date.now()) {
|
|
5487
|
+
for (const [id, value] of store) {
|
|
5488
|
+
if (value.expiresAt <= now) {
|
|
5489
|
+
store.delete(id);
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
function retainedContinuationBytes(store) {
|
|
5494
|
+
let total = 0;
|
|
5495
|
+
for (const value of store.values()) {
|
|
5496
|
+
total += value.byteLength;
|
|
5497
|
+
}
|
|
5498
|
+
return total;
|
|
5499
|
+
}
|
|
5500
|
+
function boundedResourceInteger(raw, fallback, minimum, maximum) {
|
|
5501
|
+
const parsed = raw === null ? fallback : Number.parseInt(raw, 10);
|
|
5502
|
+
return Number.isFinite(parsed)
|
|
5503
|
+
? Math.min(maximum, Math.max(minimum, parsed))
|
|
5504
|
+
: fallback;
|
|
5505
|
+
}
|
|
4863
5506
|
function jsonResource(uri, payload) {
|
|
4864
5507
|
return {
|
|
4865
5508
|
contents: [
|
|
@@ -4872,41 +5515,86 @@ function jsonResource(uri, payload) {
|
|
|
4872
5515
|
};
|
|
4873
5516
|
}
|
|
4874
5517
|
function rememberExport(exportStore, exportId, payload) {
|
|
5518
|
+
const now = Date.now();
|
|
5519
|
+
const byteLength = Buffer.byteLength(JSON.stringify(payload), "utf8");
|
|
5520
|
+
pruneExpiredExports(exportStore, now);
|
|
4875
5521
|
if (exportStore.has(exportId)) {
|
|
4876
5522
|
exportStore.delete(exportId);
|
|
4877
5523
|
}
|
|
4878
|
-
while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES
|
|
5524
|
+
while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES ||
|
|
5525
|
+
retainedExportBytes(exportStore) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
4879
5526
|
const oldestKey = exportStore.keys().next().value;
|
|
4880
5527
|
if (oldestKey === undefined) {
|
|
4881
5528
|
break;
|
|
4882
5529
|
}
|
|
4883
5530
|
exportStore.delete(oldestKey);
|
|
4884
5531
|
}
|
|
4885
|
-
exportStore.set(exportId,
|
|
5532
|
+
exportStore.set(exportId, {
|
|
5533
|
+
payload,
|
|
5534
|
+
byteLength,
|
|
5535
|
+
expiresAt: now + LOCAL_EXPORT_TTL_MS
|
|
5536
|
+
});
|
|
5537
|
+
}
|
|
5538
|
+
function pruneExpiredExports(exportStore, now = Date.now()) {
|
|
5539
|
+
for (const [exportId, storedExport] of exportStore) {
|
|
5540
|
+
if (storedExport.expiresAt <= now) {
|
|
5541
|
+
exportStore.delete(exportId);
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
function retainedExportBytes(exportStore) {
|
|
5546
|
+
let total = 0;
|
|
5547
|
+
for (const storedExport of exportStore.values()) {
|
|
5548
|
+
total += storedExport.byteLength;
|
|
5549
|
+
}
|
|
5550
|
+
return total;
|
|
4886
5551
|
}
|
|
4887
5552
|
function previewExportPayload(payload) {
|
|
5553
|
+
if (typeof payload === "string") {
|
|
5554
|
+
return previewExportText(payload);
|
|
5555
|
+
}
|
|
4888
5556
|
if (Array.isArray(payload)) {
|
|
4889
5557
|
return payload.slice(0, EXPORT_PREVIEW_LIMIT);
|
|
4890
5558
|
}
|
|
4891
|
-
|
|
5559
|
+
const object = readObject(payload);
|
|
5560
|
+
if (object && typeof object.content === "string") {
|
|
5561
|
+
return {
|
|
5562
|
+
...object,
|
|
5563
|
+
content: previewExportText(object.content)
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5566
|
+
if (object && Array.isArray(object.targets)) {
|
|
4892
5567
|
return {
|
|
4893
|
-
...
|
|
4894
|
-
targets:
|
|
5568
|
+
...object,
|
|
5569
|
+
targets: object.targets.slice(0, EXPORT_PREVIEW_LIMIT)
|
|
4895
5570
|
};
|
|
4896
5571
|
}
|
|
4897
5572
|
return payload;
|
|
4898
5573
|
}
|
|
4899
5574
|
function previewExportCount(payload) {
|
|
5575
|
+
if (typeof payload === "string") {
|
|
5576
|
+
return Math.min(Math.max(0, payload.split(/\r?\n/).length - 1), EXPORT_PREVIEW_LIMIT);
|
|
5577
|
+
}
|
|
4900
5578
|
if (Array.isArray(payload)) {
|
|
4901
5579
|
return Math.min(payload.length, EXPORT_PREVIEW_LIMIT);
|
|
4902
5580
|
}
|
|
4903
5581
|
const object = readObject(payload);
|
|
5582
|
+
if (typeof object?.content === "string") {
|
|
5583
|
+
return Math.min(Math.max(0, object.content.split(/\r?\n/).length - 1), EXPORT_PREVIEW_LIMIT);
|
|
5584
|
+
}
|
|
4904
5585
|
const targets = object?.targets;
|
|
4905
5586
|
if (Array.isArray(targets)) {
|
|
4906
5587
|
return Math.min(targets.length, EXPORT_PREVIEW_LIMIT);
|
|
4907
5588
|
}
|
|
4908
5589
|
return payload === undefined || payload === null ? 0 : 1;
|
|
4909
5590
|
}
|
|
5591
|
+
function previewExportText(payload) {
|
|
5592
|
+
const lines = payload.split(/\r?\n/);
|
|
5593
|
+
const preview = lines.slice(0, EXPORT_PREVIEW_LIMIT + 1).join("\n");
|
|
5594
|
+
return lines.length > EXPORT_PREVIEW_LIMIT + 1
|
|
5595
|
+
? `${preview}\n[preview truncated; read the export resource for the complete payload]`
|
|
5596
|
+
: preview;
|
|
5597
|
+
}
|
|
4910
5598
|
function collectResourceLinks(payload) {
|
|
4911
5599
|
const links = new Map();
|
|
4912
5600
|
for (const program of extractProgramsFromPayload(payload)) {
|
|
@@ -4948,6 +5636,19 @@ function collectResourceLinks(payload) {
|
|
|
4948
5636
|
mimeType: "application/json"
|
|
4949
5637
|
});
|
|
4950
5638
|
}
|
|
5639
|
+
const continuation = readObject(payload.continuation);
|
|
5640
|
+
const continuationUri = stringField(continuation, "resource_uri");
|
|
5641
|
+
const continuationId = stringField(continuation, "continuation_id");
|
|
5642
|
+
if (continuationUri && continuationId) {
|
|
5643
|
+
links.set(continuationUri, {
|
|
5644
|
+
type: "resource_link",
|
|
5645
|
+
uri: continuationUri,
|
|
5646
|
+
name: `${continuationId} continuation`,
|
|
5647
|
+
title: "Result continuation",
|
|
5648
|
+
description: "Read the next bounded page of this local in-memory result.",
|
|
5649
|
+
mimeType: "application/json"
|
|
5650
|
+
});
|
|
5651
|
+
}
|
|
4951
5652
|
return [...links.values()];
|
|
4952
5653
|
}
|
|
4953
5654
|
function extractProgramsFromPayload(payload) {
|
|
@@ -4997,9 +5698,18 @@ function compactProgram(program) {
|
|
|
4997
5698
|
handle: stringField(program, "handle"),
|
|
4998
5699
|
name: stringField(program, "name"),
|
|
4999
5700
|
platform: stringField(program, "platform"),
|
|
5701
|
+
catalog: stringField(program, "catalog"),
|
|
5702
|
+
program_type: stringField(program, "program_type"),
|
|
5000
5703
|
platform_url: stringField(program, "platform_url"),
|
|
5001
5704
|
bbradar_url: stringField(program, "bbradar_url"),
|
|
5705
|
+
policy_url: stringField(program, "policy_url"),
|
|
5706
|
+
reporting_url: stringField(program, "reporting_url"),
|
|
5707
|
+
reward_status: stringField(program, "reward_status"),
|
|
5708
|
+
data_capabilities: readObject(program.data_capabilities),
|
|
5002
5709
|
opportunity_tag: readObject(program.opportunity_tag),
|
|
5710
|
+
competition_risk: stringField(program, "competition_risk"),
|
|
5711
|
+
competition_risk_reason: stringField(program, "competition_risk_reason"),
|
|
5712
|
+
submission_cost: readObject(program.submission_cost),
|
|
5003
5713
|
reward_range: readObject(program.reward_range),
|
|
5004
5714
|
public_report_count: readNumber(program.public_report_count),
|
|
5005
5715
|
public_report_count_label: stringField(program, "public_report_count_label"),
|
|
@@ -5049,6 +5759,30 @@ function formatChange(change, mode) {
|
|
|
5049
5759
|
target: target ? compactTarget(target) : undefined
|
|
5050
5760
|
});
|
|
5051
5761
|
}
|
|
5762
|
+
function formatRepositoryChange(change, mode) {
|
|
5763
|
+
if (mode === "full") {
|
|
5764
|
+
return change;
|
|
5765
|
+
}
|
|
5766
|
+
const repository = readObject(change.repository);
|
|
5767
|
+
return stripUndefined({
|
|
5768
|
+
id: change.id,
|
|
5769
|
+
event_type: stringField(change, "event_type"),
|
|
5770
|
+
ref_name: stringField(change, "ref_name"),
|
|
5771
|
+
observed_at: stringField(change, "observed_at"),
|
|
5772
|
+
repository: repository
|
|
5773
|
+
? stripUndefined({
|
|
5774
|
+
provider: stringField(repository, "provider"),
|
|
5775
|
+
full_name: stringField(repository, "full_name"),
|
|
5776
|
+
url: stringField(repository, "url")
|
|
5777
|
+
})
|
|
5778
|
+
: undefined,
|
|
5779
|
+
changes_url: stringField(change, "changes_url"),
|
|
5780
|
+
programs: readArray(change.programs).map((program) => formatProgram(readObject(program) ?? {}, "compact")),
|
|
5781
|
+
targets: Array.isArray(change.targets)
|
|
5782
|
+
? change.targets.map((target) => compactTarget(readObject(target) ?? {}))
|
|
5783
|
+
: undefined
|
|
5784
|
+
});
|
|
5785
|
+
}
|
|
5052
5786
|
function buildTargetScopeSummary(targets, mode, groupLimit, includeOutOfScope, includeIneligible) {
|
|
5053
5787
|
const bountyTargets = targets.filter((target) => target.in_scope === true && target.eligible_for_bounty === true);
|
|
5054
5788
|
const groups = {
|
|
@@ -5300,6 +6034,14 @@ function candidateLooksContestLike(candidate) {
|
|
|
5300
6034
|
return tagIncludesAnyNormalized(candidate.normalizedSearchSignals, WEB3_CONTEST_SIGNALS);
|
|
5301
6035
|
}
|
|
5302
6036
|
function candidateHasKnownNoBounty(candidate) {
|
|
6037
|
+
const programType = normalizeTag(stringField(candidate.program, "program_type") ?? "");
|
|
6038
|
+
const rewardStatus = normalizeTag(stringField(candidate.program, "reward_status") ?? "");
|
|
6039
|
+
if (programType === "vdp" || rewardStatus === "no_reward" || rewardStatus === "no reward") {
|
|
6040
|
+
return true;
|
|
6041
|
+
}
|
|
6042
|
+
if (programType === "paid" || rewardStatus === "paid") {
|
|
6043
|
+
return false;
|
|
6044
|
+
}
|
|
5303
6045
|
if (candidate.bountyMin === undefined && candidate.bountyMax === undefined) {
|
|
5304
6046
|
return false;
|
|
5305
6047
|
}
|
|
@@ -5372,6 +6114,9 @@ function hasHighConfidenceProgramResolutionMatch(programs, webBaseUrl, query) {
|
|
|
5372
6114
|
const threshold = looksLikeDirectProgramHandleQuery(query) ? 95 : 85;
|
|
5373
6115
|
return programs.some((program) => programResolutionMatch(toProgramCandidate(program, webBaseUrl), query).score >= threshold);
|
|
5374
6116
|
}
|
|
6117
|
+
function hasViableProgramResolutionMatch(programs, scorePrograms) {
|
|
6118
|
+
return scorePrograms ? scorePrograms(programs) >= 80 : programs.length > 0;
|
|
6119
|
+
}
|
|
5375
6120
|
function bestProgramResolutionScore(programs, webBaseUrl, query) {
|
|
5376
6121
|
return programs.reduce((best, program) => Math.max(best, programResolutionMatch(toProgramCandidate(program, webBaseUrl), query).score), 0);
|
|
5377
6122
|
}
|
|
@@ -5485,6 +6230,16 @@ function normalizeFindProgramsInput(input) {
|
|
|
5485
6230
|
const warnings = [];
|
|
5486
6231
|
const normalized = { ...input };
|
|
5487
6232
|
normalized.vdp_mode ??= "exclude";
|
|
6233
|
+
normalized.exclude_platforms = uniqueStrings(normalized.exclude_platforms ?? []);
|
|
6234
|
+
if (normalized.web3 && normalized.exclude_web3) {
|
|
6235
|
+
normalized.web3 = false;
|
|
6236
|
+
warnings.push("exclude_web3=true took precedence over the contradictory web3=true filter.");
|
|
6237
|
+
}
|
|
6238
|
+
const includedPlatforms = new Set(normalized.platforms.map(normalizeTag));
|
|
6239
|
+
const overlappingPlatforms = normalized.exclude_platforms.filter((platform) => includedPlatforms.has(normalizeTag(platform)));
|
|
6240
|
+
if (overlappingPlatforms.length > 0) {
|
|
6241
|
+
warnings.push(`Excluded platform filters take precedence over included values: ${overlappingPlatforms.join(", ")}.`);
|
|
6242
|
+
}
|
|
5488
6243
|
normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
|
|
5489
6244
|
normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
|
|
5490
6245
|
normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
|
|
@@ -5511,7 +6266,13 @@ async function findPrograms(client, config, input) {
|
|
|
5511
6266
|
initial: input.upstream_request_budget,
|
|
5512
6267
|
remaining: input.upstream_request_budget
|
|
5513
6268
|
};
|
|
5514
|
-
const upstreamTags = uniqueStrings([
|
|
6269
|
+
const upstreamTags = uniqueStrings([
|
|
6270
|
+
...input.tags,
|
|
6271
|
+
...input.scope_tags,
|
|
6272
|
+
...input.target_types,
|
|
6273
|
+
...input.language_tags,
|
|
6274
|
+
...(input.min_wildcards > 0 ? ["wildcard"] : [])
|
|
6275
|
+
]);
|
|
5515
6276
|
const collected = await collectPrograms(client, {
|
|
5516
6277
|
platforms: input.platforms,
|
|
5517
6278
|
tags: upstreamTags,
|
|
@@ -5545,8 +6306,10 @@ async function findPrograms(client, config, input) {
|
|
|
5545
6306
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
5546
6307
|
filters: stripUndefined({
|
|
5547
6308
|
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
6309
|
+
exclude_platforms: input.exclude_platforms && input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
|
|
5548
6310
|
tags: upstreamTags.length > 0 ? upstreamTags : undefined,
|
|
5549
6311
|
web3: input.web3 ? true : undefined,
|
|
6312
|
+
exclude_web3: input.exclude_web3 ? true : undefined,
|
|
5550
6313
|
opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
|
|
5551
6314
|
updated_since: input.updated_since,
|
|
5552
6315
|
min_bounty_min: input.min_bounty_min,
|
|
@@ -5623,8 +6386,33 @@ async function collectProgramsForResolution(client, options) {
|
|
|
5623
6386
|
const collections = [];
|
|
5624
6387
|
const queryCollections = [];
|
|
5625
6388
|
const queries = options.searchQueries.length > 0 ? options.searchQueries : [undefined];
|
|
6389
|
+
const fastQueries = queries.slice(0, FAST_RESOLVE_SEARCH_QUERIES);
|
|
6390
|
+
const remainingQueries = queries.slice(FAST_RESOLVE_SEARCH_QUERIES);
|
|
5626
6391
|
const primaryDirectPlatforms = options.platforms.length > 0 ? options.platforms : PRIMARY_PROGRAM_ID_PLATFORM_GUESSES;
|
|
5627
6392
|
const primaryDirectProgramIds = new Set(directProgramIdCandidates(options.query, primaryDirectPlatforms, { maxHandleVariants: 1 }));
|
|
6393
|
+
for (const search of fastQueries) {
|
|
6394
|
+
if (options.budget.remaining <= 0) {
|
|
6395
|
+
addUniqueWarning(options.warnings, "Program name search stopped at the request budget; pass a higher upstream_request_budget for a deeper resolution attempt.");
|
|
6396
|
+
break;
|
|
6397
|
+
}
|
|
6398
|
+
const collection = await collectPrograms(client, {
|
|
6399
|
+
search,
|
|
6400
|
+
platforms: options.platforms,
|
|
6401
|
+
tags: options.tags,
|
|
6402
|
+
updated_since: undefined,
|
|
6403
|
+
opportunity_levels: options.opportunity_levels,
|
|
6404
|
+
max_pages: 1,
|
|
6405
|
+
budget: options.budget,
|
|
6406
|
+
warnings: options.warnings
|
|
6407
|
+
});
|
|
6408
|
+
collections.push(collection);
|
|
6409
|
+
queryCollections.push({ search, collection });
|
|
6410
|
+
const merged = mergeProgramCollections(collections);
|
|
6411
|
+
if (options.isHighConfidenceMatch?.(merged.programs) ||
|
|
6412
|
+
hasViableProgramResolutionMatch(merged.programs, options.scorePrograms)) {
|
|
6413
|
+
return merged;
|
|
6414
|
+
}
|
|
6415
|
+
}
|
|
5628
6416
|
if (primaryDirectProgramIds.size > 0 && options.budget.remaining > 0) {
|
|
5629
6417
|
const primaryDirectHandles = await collectDirectHandlePrograms(client, {
|
|
5630
6418
|
query: options.query,
|
|
@@ -5644,7 +6432,7 @@ async function collectProgramsForResolution(client, options) {
|
|
|
5644
6432
|
return mergeProgramCollections(collections);
|
|
5645
6433
|
}
|
|
5646
6434
|
}
|
|
5647
|
-
for (const search of
|
|
6435
|
+
for (const search of remainingQueries) {
|
|
5648
6436
|
if (options.budget.remaining <= 0) {
|
|
5649
6437
|
addUniqueWarning(options.warnings, "Program name search stopped at the request budget; pass a higher upstream_request_budget for a deeper resolution attempt.");
|
|
5650
6438
|
break;
|
|
@@ -5665,7 +6453,9 @@ async function collectProgramsForResolution(client, options) {
|
|
|
5665
6453
|
return mergeProgramCollections(collections);
|
|
5666
6454
|
}
|
|
5667
6455
|
}
|
|
5668
|
-
|
|
6456
|
+
const searchedCollection = collections.length > 0 ? mergeProgramCollections(collections) : undefined;
|
|
6457
|
+
if (options.budget.remaining > 0 &&
|
|
6458
|
+
(!searchedCollection || !hasViableProgramResolutionMatch(searchedCollection.programs, options.scorePrograms))) {
|
|
5669
6459
|
const directHandles = await collectDirectHandlePrograms(client, {
|
|
5670
6460
|
query: options.query,
|
|
5671
6461
|
platforms: options.platforms,
|
|
@@ -5752,9 +6542,14 @@ async function collectDirectHandlePrograms(client, options) {
|
|
|
5752
6542
|
const sourceRequests = [];
|
|
5753
6543
|
let requestsScanned = 0;
|
|
5754
6544
|
for (const programId of programIds) {
|
|
6545
|
+
if (!tryConsumeBudget(options.budget)) {
|
|
6546
|
+
addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all direct program handle guesses could be checked.");
|
|
6547
|
+
break;
|
|
6548
|
+
}
|
|
5755
6549
|
requestsScanned += 1;
|
|
5756
6550
|
try {
|
|
5757
6551
|
const targetsApi = await client.getProgramTargets(programId);
|
|
6552
|
+
refundBudgetIfNoUpstreamRequest(options.budget, targetsApi);
|
|
5758
6553
|
const targetsData = readObject(targetsApi.data);
|
|
5759
6554
|
const hasTargetsArray = Array.isArray(targetsData?.targets);
|
|
5760
6555
|
const targets = readArray(targetsData?.targets).map(sanitizeTarget);
|
|
@@ -5775,6 +6570,7 @@ async function collectDirectHandlePrograms(client, options) {
|
|
|
5775
6570
|
}
|
|
5776
6571
|
}
|
|
5777
6572
|
catch (error) {
|
|
6573
|
+
refundBudgetIfNoUpstreamError(options.budget, error);
|
|
5778
6574
|
if (error instanceof BBRadarApiError && error.status === 404) {
|
|
5779
6575
|
sourceRequests.push({
|
|
5780
6576
|
source: options.source,
|
|
@@ -5782,7 +6578,9 @@ async function collectDirectHandlePrograms(client, options) {
|
|
|
5782
6578
|
program_id: programId,
|
|
5783
6579
|
request_id: error.requestId,
|
|
5784
6580
|
upstream_request_id: error.upstreamRequestId,
|
|
5785
|
-
status: error.status
|
|
6581
|
+
status: error.status,
|
|
6582
|
+
cache_hit: error.cached,
|
|
6583
|
+
cache_expires_at: error.cacheExpiresAt
|
|
5786
6584
|
});
|
|
5787
6585
|
continue;
|
|
5788
6586
|
}
|
|
@@ -6043,12 +6841,19 @@ function programCandidateMatches(candidate, filters) {
|
|
|
6043
6841
|
if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
6044
6842
|
return false;
|
|
6045
6843
|
}
|
|
6844
|
+
if ((filters.exclude_platforms?.length ?? 0) > 0 &&
|
|
6845
|
+
filters.exclude_platforms?.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
6846
|
+
return false;
|
|
6847
|
+
}
|
|
6046
6848
|
if (filters.tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.tags)) {
|
|
6047
6849
|
return false;
|
|
6048
6850
|
}
|
|
6049
6851
|
if (filters.web3 && !candidateHasWeb3Surface(candidate)) {
|
|
6050
6852
|
return false;
|
|
6051
6853
|
}
|
|
6854
|
+
if (filters.exclude_web3 && candidateHasWeb3Surface(candidate)) {
|
|
6855
|
+
return false;
|
|
6856
|
+
}
|
|
6052
6857
|
if (filters.scope_tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.scope_tags)) {
|
|
6053
6858
|
return false;
|
|
6054
6859
|
}
|
|
@@ -6061,7 +6866,9 @@ function programCandidateMatches(candidate, filters) {
|
|
|
6061
6866
|
if (candidate.wildcardCount < filters.min_wildcards) {
|
|
6062
6867
|
return false;
|
|
6063
6868
|
}
|
|
6064
|
-
|
|
6869
|
+
const explicitlyPaid = normalizeTag(stringField(candidate.program, "program_type") ?? "") === "paid" ||
|
|
6870
|
+
normalizeTag(stringField(candidate.program, "reward_status") ?? "") === "paid";
|
|
6871
|
+
if (filters.require_bounty && !explicitlyPaid && (candidate.bountyMax ?? candidate.bountyMin ?? 0) <= 0) {
|
|
6065
6872
|
return false;
|
|
6066
6873
|
}
|
|
6067
6874
|
if (filters.min_bounty_min !== undefined && (candidate.bountyMin ?? 0) < filters.min_bounty_min) {
|
|
@@ -6195,7 +7002,8 @@ async function collectTargetSamples(client, candidates, input, budget) {
|
|
|
6195
7002
|
.slice(0, input.target_sample_size);
|
|
6196
7003
|
samples[programId] = targets;
|
|
6197
7004
|
}
|
|
6198
|
-
catch {
|
|
7005
|
+
catch (error) {
|
|
7006
|
+
refundBudgetIfNoUpstreamError(budget, error);
|
|
6199
7007
|
samples[programId] = [];
|
|
6200
7008
|
}
|
|
6201
7009
|
});
|
|
@@ -6258,6 +7066,11 @@ function refundBudgetIfNoUpstreamRequest(budget, api, cost = 1) {
|
|
|
6258
7066
|
budget.remaining = Math.min(budget.initial, budget.remaining + cost);
|
|
6259
7067
|
}
|
|
6260
7068
|
}
|
|
7069
|
+
function refundBudgetIfNoUpstreamError(budget, error, cost = 1) {
|
|
7070
|
+
if (error instanceof BBRadarApiError && (error.cached || error.coalesced)) {
|
|
7071
|
+
budget.remaining = Math.min(budget.initial, budget.remaining + cost);
|
|
7072
|
+
}
|
|
7073
|
+
}
|
|
6261
7074
|
function addUniqueWarning(warnings, warning) {
|
|
6262
7075
|
if (warnings && !warnings.includes(warning)) {
|
|
6263
7076
|
warnings.push(warning);
|
|
@@ -6360,7 +7173,12 @@ function readStringArrayField(value, field) {
|
|
|
6360
7173
|
const fieldValue = value[field];
|
|
6361
7174
|
return Array.isArray(fieldValue) ? fieldValue.filter((entry) => typeof entry === "string") : [];
|
|
6362
7175
|
}
|
|
6363
|
-
function registerPrompts(server) {
|
|
7176
|
+
function registerPrompts(server, profile) {
|
|
7177
|
+
const discoveryToolInstructions = profile === "core"
|
|
7178
|
+
? `- Start with find_programs and pass opportunity_levels from the requested value below unless the user asked for a broader search.
|
|
7179
|
+
- Express language, target type, reward, Web3, wildcard, VDP, and platform needs through find_programs filters.`
|
|
7180
|
+
: `- Start with find_program_candidates unless the user needs custom filters; pass opportunity_levels from the requested value below.
|
|
7181
|
+
- Use find_language_programs for language asks, find_target_type_programs for API/mobile/source-code/domain asks, find_reward_programs or find_paid_programs for reward asks, find_vdp_programs only for explicit VDP/no-bounty asks, find_web3_contests for Web3 contest/audit asks, find_web3_programs for general Web3 requests, and find_wildcard_programs for wildcard-specific requests.`;
|
|
6364
7182
|
server.registerPrompt("find_best_programs_to_hunt", {
|
|
6365
7183
|
title: "Find Best Programs To Hunt",
|
|
6366
7184
|
description: "Rank fresh BBRadar program data only. Do not invoke external bug bounty skills unless the user explicitly asks for methodology.",
|
|
@@ -6373,8 +7191,8 @@ function registerPrompts(server) {
|
|
|
6373
7191
|
}, (args) => promptResult(`Use the BBRadar MCP tools only. Do not invoke local bug bounty skills, methodology files, worklogs, or non-BBRadar tools unless the user explicitly asks for methodology. Do not perform active scanning or contact third-party targets. Treat results as fresh API reads; do not describe them as cached.
|
|
6374
7192
|
|
|
6375
7193
|
Find the best BBRadar program candidates.
|
|
6376
|
-
|
|
6377
|
-
- Use
|
|
7194
|
+
${discoveryToolInstructions}
|
|
7195
|
+
- Use this opportunity level JSON array: ${promptJson([args.opportunity_level ?? "elite"])}.
|
|
6378
7196
|
- Apply platform filters from this JSON string if provided: ${promptJson(args.platforms ?? "")}.
|
|
6379
7197
|
- Apply tag filters from this JSON string if provided: ${promptJson(args.tags ?? "")}.
|
|
6380
7198
|
- Keep the shortlist to this JSON value: ${promptJson(args.max_programs ?? "10")}.
|
|
@@ -6421,6 +7239,111 @@ The plan must:
|
|
|
6421
7239
|
- Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
|
|
6422
7240
|
- Include a short checklist of BBRadar follow-up tool calls and decision points.`));
|
|
6423
7241
|
}
|
|
7242
|
+
function resolveFeedCursor(store, endpoint, filters, resume) {
|
|
7243
|
+
const normalizedFilters = stableStringify(normalizeFeedCursorFilters(filters));
|
|
7244
|
+
const key = `${endpoint}:${createHash("sha256").update(normalizedFilters).digest("hex")}`;
|
|
7245
|
+
const explicitAfterId = readNumber(filters.after_id);
|
|
7246
|
+
if (explicitAfterId !== undefined) {
|
|
7247
|
+
return { endpoint, key, afterId: explicitAfterId, resumed: false };
|
|
7248
|
+
}
|
|
7249
|
+
if (!resume) {
|
|
7250
|
+
return { endpoint, key, afterId: undefined, resumed: false };
|
|
7251
|
+
}
|
|
7252
|
+
const stored = store.get(key);
|
|
7253
|
+
if (stored === undefined) {
|
|
7254
|
+
throw new Error(`No in-memory ${endpoint} cursor exists for this filter set. Provide after_id once to seed it.`);
|
|
7255
|
+
}
|
|
7256
|
+
return { endpoint, key, afterId: stored, resumed: true };
|
|
7257
|
+
}
|
|
7258
|
+
function advanceFeedCursor(store, cursor, rawMeta) {
|
|
7259
|
+
if (cursor.afterId === undefined) {
|
|
7260
|
+
return undefined;
|
|
7261
|
+
}
|
|
7262
|
+
const meta = readObject(rawMeta);
|
|
7263
|
+
const nextAfterId = readNumber(meta?.next_after_id) ?? cursor.afterId;
|
|
7264
|
+
store.set(cursor.key, nextAfterId);
|
|
7265
|
+
return {
|
|
7266
|
+
endpoint: cursor.endpoint,
|
|
7267
|
+
mode: cursor.resumed ? "resumed" : "seeded",
|
|
7268
|
+
used_after_id: cursor.afterId,
|
|
7269
|
+
next_after_id: nextAfterId,
|
|
7270
|
+
has_more: readBoolean(meta?.has_more) ?? false,
|
|
7271
|
+
stored_in_memory: true
|
|
7272
|
+
};
|
|
7273
|
+
}
|
|
7274
|
+
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, fetchPage) {
|
|
7275
|
+
const startedAt = Date.now();
|
|
7276
|
+
const results = [];
|
|
7277
|
+
const sourceRequests = [];
|
|
7278
|
+
let currentCursor = initialCursor;
|
|
7279
|
+
let lastApi;
|
|
7280
|
+
let lastData;
|
|
7281
|
+
let cursorPayload;
|
|
7282
|
+
let pagesFetched = 0;
|
|
7283
|
+
let hasMore = false;
|
|
7284
|
+
let pageBudgetExhausted = false;
|
|
7285
|
+
let timeBudgetExhausted = false;
|
|
7286
|
+
do {
|
|
7287
|
+
const api = await fetchPage(currentCursor.afterId);
|
|
7288
|
+
const data = readObject(api.data);
|
|
7289
|
+
const pageResults = readArray(data?.results);
|
|
7290
|
+
lastApi = api;
|
|
7291
|
+
lastData = data;
|
|
7292
|
+
results.push(...pageResults);
|
|
7293
|
+
pagesFetched += 1;
|
|
7294
|
+
sourceRequests.push({
|
|
7295
|
+
source: currentCursor.endpoint,
|
|
7296
|
+
page: pagesFetched,
|
|
7297
|
+
request_id: api.requestId,
|
|
7298
|
+
upstream_request_id: api.upstreamRequestId,
|
|
7299
|
+
...apiSourceMetadata(api)
|
|
7300
|
+
});
|
|
7301
|
+
cursorPayload = advanceFeedCursor(store, currentCursor, data?.meta);
|
|
7302
|
+
hasMore = readBoolean(readObject(data?.meta)?.has_more) === true;
|
|
7303
|
+
if (currentCursor.afterId === undefined || !hasMore) {
|
|
7304
|
+
break;
|
|
7305
|
+
}
|
|
7306
|
+
if (pagesFetched >= maxPages) {
|
|
7307
|
+
pageBudgetExhausted = true;
|
|
7308
|
+
break;
|
|
7309
|
+
}
|
|
7310
|
+
if (Date.now() - startedAt >= timeBudgetMs) {
|
|
7311
|
+
timeBudgetExhausted = true;
|
|
7312
|
+
break;
|
|
7313
|
+
}
|
|
7314
|
+
currentCursor = {
|
|
7315
|
+
...currentCursor,
|
|
7316
|
+
afterId: readNumber(cursorPayload?.next_after_id) ?? currentCursor.afterId,
|
|
7317
|
+
resumed: true
|
|
7318
|
+
};
|
|
7319
|
+
} while (true);
|
|
7320
|
+
if (!lastApi) {
|
|
7321
|
+
throw new Error(`No ${initialCursor.endpoint} response was collected.`);
|
|
7322
|
+
}
|
|
7323
|
+
return {
|
|
7324
|
+
lastApi,
|
|
7325
|
+
lastData,
|
|
7326
|
+
results,
|
|
7327
|
+
cursor: cursorPayload,
|
|
7328
|
+
pagesFetched,
|
|
7329
|
+
pageBudgetExhausted,
|
|
7330
|
+
timeBudgetExhausted,
|
|
7331
|
+
sourceRequests
|
|
7332
|
+
};
|
|
7333
|
+
}
|
|
7334
|
+
function normalizeFeedCursorFilters(filters) {
|
|
7335
|
+
return Object.fromEntries(Object.entries(filters)
|
|
7336
|
+
.filter(([key, value]) => key !== "after_id" && key !== "page" && value !== undefined)
|
|
7337
|
+
.map(([key, value]) => [key, normalizeFeedCursorFilterValue(value)]));
|
|
7338
|
+
}
|
|
7339
|
+
function normalizeFeedCursorFilterValue(value) {
|
|
7340
|
+
if (Array.isArray(value)) {
|
|
7341
|
+
return value
|
|
7342
|
+
.map((entry) => normalizeFeedCursorFilterValue(entry))
|
|
7343
|
+
.sort((left, right) => String(left).localeCompare(String(right)));
|
|
7344
|
+
}
|
|
7345
|
+
return typeof value === "string" ? value.trim().toLowerCase() : value;
|
|
7346
|
+
}
|
|
6424
7347
|
async function runTool(toolName, config, rateLimiter, callback) {
|
|
6425
7348
|
const startedAt = Date.now();
|
|
6426
7349
|
const decision = rateLimiter.consume(toolName, rateLimitForTool(toolName, config));
|
|
@@ -6434,7 +7357,7 @@ async function runTool(toolName, config, rateLimiter, callback) {
|
|
|
6434
7357
|
},
|
|
6435
7358
|
mcp_rate_limit: toRateLimitPayload(decision),
|
|
6436
7359
|
mcp_timing: toolTimingPayload(toolName, durationMs)
|
|
6437
|
-
}, true);
|
|
7360
|
+
}, true, config.compactToolText);
|
|
6438
7361
|
}
|
|
6439
7362
|
try {
|
|
6440
7363
|
const payload = await callback();
|
|
@@ -6444,21 +7367,21 @@ async function runTool(toolName, config, rateLimiter, callback) {
|
|
|
6444
7367
|
...payload,
|
|
6445
7368
|
mcp_rate_limit: toRateLimitPayload(decision),
|
|
6446
7369
|
mcp_timing: toolTimingPayload(toolName, durationMs)
|
|
6447
|
-
});
|
|
7370
|
+
}, false, config.compactToolText);
|
|
6448
7371
|
}
|
|
6449
7372
|
catch (error) {
|
|
6450
7373
|
const durationMs = Date.now() - startedAt;
|
|
6451
7374
|
recordToolMetric(toolName, durationMs, "error");
|
|
6452
|
-
return toolResult(errorPayload(error, decision, toolName, durationMs), true);
|
|
7375
|
+
return toolResult(errorPayload(error, decision, toolName, durationMs), true, config.compactToolText);
|
|
6453
7376
|
}
|
|
6454
7377
|
}
|
|
6455
|
-
function toolResult(payload, isError = false) {
|
|
7378
|
+
function toolResult(payload, isError = false, compactText = false) {
|
|
6456
7379
|
const resourceLinks = collectResourceLinks(payload);
|
|
6457
7380
|
return {
|
|
6458
7381
|
content: [
|
|
6459
7382
|
{
|
|
6460
7383
|
type: "text",
|
|
6461
|
-
text: JSON.stringify(payload)
|
|
7384
|
+
text: compactText ? compactToolText(payload, isError) : JSON.stringify(payload)
|
|
6462
7385
|
},
|
|
6463
7386
|
...resourceLinks
|
|
6464
7387
|
],
|
|
@@ -6466,6 +7389,29 @@ function toolResult(payload, isError = false) {
|
|
|
6466
7389
|
...(isError ? { isError: true } : {})
|
|
6467
7390
|
};
|
|
6468
7391
|
}
|
|
7392
|
+
function compactToolText(payload, isError) {
|
|
7393
|
+
if (isError) {
|
|
7394
|
+
return JSON.stringify(stripUndefined({
|
|
7395
|
+
request_id: payload.request_id,
|
|
7396
|
+
error: payload.error,
|
|
7397
|
+
mcp_timing: payload.mcp_timing
|
|
7398
|
+
}));
|
|
7399
|
+
}
|
|
7400
|
+
const meta = readObject(payload.meta);
|
|
7401
|
+
const returned = numberField(meta, "returned") ??
|
|
7402
|
+
[
|
|
7403
|
+
readArray(payload.programs).length,
|
|
7404
|
+
readArray(payload.targets).length,
|
|
7405
|
+
readArray(payload.changes).length,
|
|
7406
|
+
readArray(payload.repository_changes).length
|
|
7407
|
+
].find((count) => count > 0);
|
|
7408
|
+
return JSON.stringify(stripUndefined({
|
|
7409
|
+
request_id: payload.request_id,
|
|
7410
|
+
summary: "Full tool result is available in structuredContent.",
|
|
7411
|
+
returned,
|
|
7412
|
+
mcp_timing: payload.mcp_timing
|
|
7413
|
+
}));
|
|
7414
|
+
}
|
|
6469
7415
|
function promptResult(text) {
|
|
6470
7416
|
return {
|
|
6471
7417
|
messages: [
|
|
@@ -6488,6 +7434,7 @@ function withApiMetadata(api, payload) {
|
|
|
6488
7434
|
upstream_request_id: api.upstreamRequestId,
|
|
6489
7435
|
cache: stripUndefined({
|
|
6490
7436
|
hit: api.cached,
|
|
7437
|
+
stale_while_revalidate: api.stale,
|
|
6491
7438
|
coalesced_live_request: api.coalesced,
|
|
6492
7439
|
expires_at: api.cacheExpiresAt
|
|
6493
7440
|
}),
|
|
@@ -6605,18 +7552,25 @@ function compactRecord(values) {
|
|
|
6605
7552
|
}));
|
|
6606
7553
|
}
|
|
6607
7554
|
function latestAddedTargetsQuery(input) {
|
|
6608
|
-
return {
|
|
7555
|
+
return stripUndefined({
|
|
6609
7556
|
change_type: "added",
|
|
6610
7557
|
target_type: input.target_type,
|
|
7558
|
+
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
7559
|
+
min_reward: input.min_reward,
|
|
7560
|
+
reward_threshold_mode: input.reward_threshold_mode,
|
|
7561
|
+
max_public_report_count: input.max_public_report_count,
|
|
7562
|
+
require_known_report_count: input.require_known_report_count,
|
|
7563
|
+
include_unknown_report_count: input.include_unknown_report_count,
|
|
6611
7564
|
include_out_of_scope: input.include_out_of_scope,
|
|
6612
7565
|
include_ineligible: input.include_ineligible,
|
|
6613
7566
|
recent_changes_page_size: input.recent_changes_page_size,
|
|
7567
|
+
max_results: input.max_results,
|
|
6614
7568
|
include_full_target_list: input.include_full_target_list,
|
|
6615
7569
|
full_target_list_mode: input.full_target_list_mode,
|
|
6616
7570
|
full_target_limit: input.full_target_limit,
|
|
6617
7571
|
full_list_include_out_of_scope: input.full_list_include_out_of_scope,
|
|
6618
7572
|
full_list_include_ineligible: input.full_list_include_ineligible
|
|
6619
|
-
};
|
|
7573
|
+
});
|
|
6620
7574
|
}
|
|
6621
7575
|
function formatTargetList(targets, mode) {
|
|
6622
7576
|
if (mode === "identifiers") {
|
|
@@ -6633,7 +7587,11 @@ function compactTarget(target) {
|
|
|
6633
7587
|
target_type: stringField(target, "target_type"),
|
|
6634
7588
|
in_scope: readBoolean(target.in_scope),
|
|
6635
7589
|
eligible_for_bounty: readBoolean(target.eligible_for_bounty),
|
|
6636
|
-
wildcard: readBoolean(target.wildcard)
|
|
7590
|
+
wildcard: readBoolean(target.wildcard),
|
|
7591
|
+
target_opportunity_score: readNumber(target.target_opportunity_score),
|
|
7592
|
+
target_opportunity_label: stringField(target, "target_opportunity_label"),
|
|
7593
|
+
competition_risk: stringField(target, "competition_risk"),
|
|
7594
|
+
why_this_target: stringField(target, "why_this_target")
|
|
6637
7595
|
});
|
|
6638
7596
|
}
|
|
6639
7597
|
function targetIdentifier(target) {
|