@bbradar/mcp 0.1.13 → 0.1.19

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/dist/server.js CHANGED
@@ -20,6 +20,7 @@ const MAX_FIND_TARGET_SAMPLE_PROGRAMS = 10;
20
20
  const MAX_FIND_TARGET_SAMPLES = 20;
21
21
  const MAX_FIND_UPSTREAM_REQUEST_BUDGET = 50;
22
22
  const DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET = 20;
23
+ const DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET = 4;
23
24
  const MAX_ACCEPTED_NUMERIC_LIMIT = 1_000;
24
25
  const PROGRAM_COLLECTION_CONCURRENCY = 4;
25
26
  const TARGET_SAMPLE_CONCURRENCY = 3;
@@ -30,10 +31,34 @@ const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
30
31
  const MAX_LOCAL_EXPORT_RESOURCES = 25;
31
32
  const EXPORT_PREVIEW_LIMIT = 25;
32
33
  const STALE_PROGRAM_ID_RESOLVE_PAGES = 1;
33
- const STALE_PROGRAM_ID_RESOLVE_BUDGET = 1;
34
+ const STALE_PROGRAM_ID_RESOLVE_BUDGET = 8;
34
35
  const PROGRAM_FALLBACK_CACHE_MAX_ENTRIES = 200;
35
36
  const MAX_RESOLVE_SEARCH_QUERIES = 4;
37
+ const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
36
38
  const SDK_VERSION = "1.29.0";
39
+ const PROGRAM_ID_PLATFORM_GUESSES = [
40
+ "HackerOne",
41
+ "BugCrowd",
42
+ "Intigriti",
43
+ "HackenProof",
44
+ "YesWeHack",
45
+ "Immunefi",
46
+ "Cantina",
47
+ "Code4rena",
48
+ "Huntr",
49
+ "GObugfree",
50
+ "Bug Bounty Switzerland",
51
+ "Inspectiv",
52
+ "Compass Security",
53
+ "BugRap",
54
+ "IssueHunt",
55
+ "PatchDay",
56
+ "Remedy",
57
+ "Standoff365",
58
+ "Sherlock",
59
+ "CodeHawks"
60
+ ];
61
+ const PRIMARY_PROGRAM_ID_PLATFORM_GUESSES = PROGRAM_ID_PLATFORM_GUESSES.slice(0, 8);
37
62
  const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
38
63
  const WEB3_CONTEST_SIGNALS = ["contest", "audit", "competitive", "code4rena", "sherlock", "cantina", "codehawks", "hats", "spearbit", "warden"];
39
64
  const VDP_SIGNALS = ["vdp", "disclosure", "responsible disclosure", "vulnerability disclosure", "no bounty", "non-bounty", "no reward"];
@@ -44,6 +69,15 @@ const PROGRAM_RESOLUTION_STOP_WORDS = new Set([
44
69
  "targets",
45
70
  "scope",
46
71
  "scopes",
72
+ "network",
73
+ "networks",
74
+ "protocol",
75
+ "protocols",
76
+ "bug",
77
+ "bugs",
78
+ "bounty",
79
+ "bounties",
80
+ "security",
47
81
  "new",
48
82
  "latest",
49
83
  "recent",
@@ -138,7 +172,7 @@ function stringListSchema(description = "Optional filter values. Empty or omitte
138
172
  .describe(description);
139
173
  }
140
174
  function platformListSchema() {
141
- return stringListSchema("Optional platform filters such as HackerOne, Bugcrowd, Intigriti, or HackenProof. Empty or omitted means all platforms.");
175
+ return stringListSchema("Optional platform filters such as HackerOne, BugCrowd, Intigriti, HackenProof, Immunefi, YesWeHack, Cantina, or GObugfree. Empty or omitted means all platforms.");
142
176
  }
143
177
  function tagListSchema() {
144
178
  return stringListSchema("Optional combined scope/language/target-type tags. Empty or omitted means all tags.");
@@ -194,7 +228,7 @@ const programIdSchema = z
194
228
  .trim()
195
229
  .min(3)
196
230
  .max(256)
197
- .regex(/^[A-Za-z0-9_-]+:[^?#]+$/, "Use platform:handle, for example HackerOne:example-handle.")
231
+ .regex(/^[^:/?#\u0000-\u001F\u007F]+:[^?#\u0000-\u001F\u007F]+$/, "Use platform:handle, for example HackerOne:example-handle.")
198
232
  .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
199
233
  message: "program_id cannot contain control characters."
200
234
  })
@@ -406,7 +440,7 @@ export function createBbradarServer(client, config) {
406
440
  max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
407
441
  max_results: z.number().int().min(1).max(25).default(10),
408
442
  output_mode: programListModeSchema.default("compact"),
409
- upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
443
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET)
410
444
  },
411
445
  outputSchema: {
412
446
  ...programListOutputShape(),
@@ -439,7 +473,7 @@ export function createBbradarServer(client, config) {
439
473
  target_list_mode: targetListModeSchema.default("identifiers"),
440
474
  max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
441
475
  output_mode: programListModeSchema.default("compact"),
442
- upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
476
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET)
443
477
  },
444
478
  outputSchema: {
445
479
  ...apiEnvelopeOutputShape(),
@@ -462,7 +496,7 @@ export function createBbradarServer(client, config) {
462
496
  tags: tagListSchema(),
463
497
  opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
464
498
  max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
465
- upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET),
499
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET),
466
500
  include_out_of_scope: z.boolean().default(false),
467
501
  include_ineligible: z.boolean().default(false),
468
502
  strict_scope_filter: z.boolean().default(true),
@@ -2126,14 +2160,13 @@ async function resolveProgramNameIdentifier(client, config, requestedProgramName
2126
2160
  }
2127
2161
  const sourceRequests = readArray(resolution.source_requests).filter((request) => readObject(request) !== undefined);
2128
2162
  const warnings = readArray(resolution.warnings).filter((warning) => typeof warning === "string");
2129
- const fallbackWarning = `Resolved program name ${requestedProgramName} to program_id ${match.resolvedProgramId}.`;
2130
2163
  return {
2131
2164
  requestedProgramId: requestedProgramName,
2132
2165
  resolvedProgramId: match.resolvedProgramId,
2133
2166
  query,
2134
2167
  reason: "program_name",
2135
2168
  sourceRequests,
2136
- warnings: uniqueStrings([...warnings, fallbackWarning]),
2169
+ warnings: uniqueStrings(warnings),
2137
2170
  resolution
2138
2171
  };
2139
2172
  }
@@ -2347,6 +2380,7 @@ async function resolveProgram(client, config, input) {
2347
2380
  const maxPages = clampLimit("max_pages", input.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
2348
2381
  const searchQueries = programResolutionSearchQueries(input.query);
2349
2382
  const collected = await collectProgramsForResolution(client, {
2383
+ query: input.query,
2350
2384
  searchQueries,
2351
2385
  isHighConfidenceMatch: (programs) => hasHighConfidenceProgramResolutionMatch(programs, config.webBaseUrl, input.query),
2352
2386
  scorePrograms: (programs) => bestProgramResolutionScore(programs, config.webBaseUrl, input.query),
@@ -2357,7 +2391,9 @@ async function resolveProgram(client, config, input) {
2357
2391
  budget,
2358
2392
  warnings
2359
2393
  });
2360
- const matches = collected.programs
2394
+ const sourceRequests = [...collected.sourceRequests];
2395
+ let targetLookup;
2396
+ let matches = collected.programs
2361
2397
  .map((program) => {
2362
2398
  const candidate = toProgramCandidate(program, config.webBaseUrl);
2363
2399
  const match = programResolutionMatch(candidate, input.query);
@@ -2366,14 +2402,58 @@ async function resolveProgram(client, config, input) {
2366
2402
  .filter((entry) => entry.match.score > 0)
2367
2403
  .sort((left, right) => right.match.score - left.match.score || compareProgramCandidates(left.candidate, right.candidate, "best_hunt_value"))
2368
2404
  .slice(0, input.max_results);
2405
+ if (matches.length === 0 && queryLooksLikeTargetIdentifier(input.query)) {
2406
+ targetLookup = await findProgramsByTarget(client, config, {
2407
+ target_query: input.query,
2408
+ match_mode: "smart",
2409
+ platforms: input.platforms,
2410
+ tags: input.tags,
2411
+ use_api_search: true,
2412
+ max_pages: 1,
2413
+ max_results: input.max_results,
2414
+ include_out_of_scope: false,
2415
+ include_ineligible: false,
2416
+ max_targets_per_program: 3,
2417
+ target_list_mode: "identifiers",
2418
+ upstream_request_budget: Math.max(1, budget.remaining)
2419
+ });
2420
+ sourceRequests.push(...readArray(targetLookup.source_requests).filter((request) => readObject(request) !== undefined));
2421
+ for (const warning of readArray(targetLookup.warnings)) {
2422
+ if (typeof warning === "string") {
2423
+ addUniqueWarning(warnings, warning);
2424
+ }
2425
+ }
2426
+ const targetBudget = readObject(targetLookup.upstream_budget);
2427
+ const targetBudgetUsed = numberField(targetBudget, "used") ?? 0;
2428
+ budget.remaining = Math.max(0, budget.remaining - targetBudgetUsed);
2429
+ matches = readArray(targetLookup.matches)
2430
+ .map((entry) => {
2431
+ const matchEntry = readObject(entry);
2432
+ const program = readObject(matchEntry?.program);
2433
+ if (!program) {
2434
+ return undefined;
2435
+ }
2436
+ return {
2437
+ candidate: toProgramCandidate(program, config.webBaseUrl),
2438
+ match: {
2439
+ score: 75,
2440
+ fields: ["targets"],
2441
+ reasons: ["target search matched query"]
2442
+ }
2443
+ };
2444
+ })
2445
+ .filter((entry) => entry !== undefined)
2446
+ .slice(0, input.max_results);
2447
+ }
2369
2448
  const programs = matches.map((entry) => formatProgram(entry.candidate.program, input.output_mode));
2370
2449
  const payload = stripUndefined({
2371
2450
  request_id: randomUUID(),
2372
- source_requests: collected.sourceRequests,
2451
+ source_requests: sourceRequests,
2373
2452
  warnings: warnings.length > 0 ? warnings : undefined,
2374
2453
  query: {
2375
2454
  text: input.query,
2376
2455
  search_queries: searchQueries,
2456
+ target_search_queries: readObject(targetLookup?.filters)?.target_search_queries,
2377
2457
  platforms: input.platforms.length > 0 ? input.platforms : undefined,
2378
2458
  tags: input.tags.length > 0 ? input.tags : undefined
2379
2459
  },
@@ -2398,7 +2478,8 @@ async function resolveProgram(client, config, input) {
2398
2478
  meta: {
2399
2479
  returned: programs.length,
2400
2480
  programs_scanned: collected.programsScanned,
2401
- upstream_requests_scanned: collected.requestsScanned
2481
+ upstream_requests_scanned: collected.requestsScanned,
2482
+ target_lookup_returned: numberField(readObject(targetLookup?.meta), "returned")
2402
2483
  }
2403
2484
  });
2404
2485
  rememberResolveProgram(config, input, payload);
@@ -3091,16 +3172,44 @@ function findRecentByType(client, config, input) {
3091
3172
  }
3092
3173
  async function findProgramsByTarget(client, config, input) {
3093
3174
  const warnings = [];
3175
+ const budget = {
3176
+ initial: input.upstream_request_budget,
3177
+ remaining: input.upstream_request_budget
3178
+ };
3179
+ const sourceRequests = [];
3180
+ const matches = [];
3181
+ const directHandles = await collectDirectHandlePrograms(client, {
3182
+ query: input.target_query,
3183
+ platforms: input.platforms,
3184
+ budget,
3185
+ warnings,
3186
+ source: "target_query_program_id_guess",
3187
+ maxProgramIds: queryLooksLikeTargetIdentifier(input.target_query) || looksLikeDirectProgramHandleQuery(input.target_query) ? MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS : 4,
3188
+ maxHandleVariants: 1,
3189
+ stopOnFirstMatch: true
3190
+ });
3191
+ sourceRequests.push(...directHandles.sourceRequests);
3192
+ await appendProgramTargetMatches(client, input, budget, warnings, sourceRequests, matches, directHandles.programs.map((program) => toProgramCandidate(program, config.webBaseUrl)));
3193
+ if (matches.length > 0 && input.match_mode === "smart") {
3194
+ return programsByTargetResult(input, warnings, sourceRequests, matches, budget, {
3195
+ source: "direct_program_handle_targets",
3196
+ rankingScope: {
3197
+ mode: "direct_program_handle",
3198
+ possibly_incomplete: false
3199
+ },
3200
+ meta: {
3201
+ returned: matches.length,
3202
+ programs_scanned: directHandles.programsScanned,
3203
+ target_lists_checked: sourceRequests.filter((request) => request.source === "program_targets").length
3204
+ }
3205
+ });
3206
+ }
3094
3207
  if (input.use_api_search) {
3095
3208
  const directResult = await searchProgramsByTargetApi(client, config, input, warnings);
3096
3209
  if (directResult) {
3097
3210
  return directResult;
3098
3211
  }
3099
3212
  }
3100
- const budget = {
3101
- initial: input.upstream_request_budget,
3102
- remaining: input.upstream_request_budget
3103
- };
3104
3213
  const maxPages = clampLimit("max_pages", input.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
3105
3214
  const collected = await collectPrograms(client, {
3106
3215
  platforms: input.platforms,
@@ -3111,11 +3220,27 @@ async function findProgramsByTarget(client, config, input) {
3111
3220
  budget,
3112
3221
  warnings
3113
3222
  });
3114
- const sourceRequests = [...collected.sourceRequests];
3115
- const matches = [];
3223
+ sourceRequests.push(...collected.sourceRequests);
3116
3224
  const candidates = collected.programs
3117
3225
  .map((program) => toProgramCandidate(program, config.webBaseUrl))
3118
3226
  .sort((left, right) => compareProgramCandidates(left, right, "best_hunt_value"));
3227
+ await appendProgramTargetMatches(client, input, budget, warnings, sourceRequests, matches, candidates);
3228
+ return programsByTargetResult(input, warnings, sourceRequests, matches, budget, {
3229
+ source: "sampled_program_targets",
3230
+ rankingScope: {
3231
+ mode: "sampled",
3232
+ max_pages_scanned_per_source: maxPages,
3233
+ upstream_total_pages: collected.totalPagesBySource,
3234
+ possibly_incomplete: isRankingPossiblyIncomplete(collected.totalPagesBySource, maxPages)
3235
+ },
3236
+ meta: {
3237
+ returned: matches.length,
3238
+ programs_scanned: directHandles.programsScanned + collected.programsScanned,
3239
+ target_lists_checked: sourceRequests.filter((request) => request.source === "program_targets").length
3240
+ }
3241
+ });
3242
+ }
3243
+ async function appendProgramTargetMatches(client, input, budget, warnings, sourceRequests, matches, candidates) {
3119
3244
  for (const candidate of candidates) {
3120
3245
  if (matches.length >= input.max_results) {
3121
3246
  break;
@@ -3125,7 +3250,9 @@ async function findProgramsByTarget(client, config, input) {
3125
3250
  continue;
3126
3251
  }
3127
3252
  if (!tryConsumeBudget(budget)) {
3128
- addUniqueWarning(warnings, "Upstream request budget was exhausted before all candidate target lists could be checked.");
3253
+ if (matches.length === 0) {
3254
+ addUniqueWarning(warnings, "Upstream request budget was exhausted before all candidate target lists could be checked.");
3255
+ }
3129
3256
  break;
3130
3257
  }
3131
3258
  const targetsApi = await client.getProgramTargets(programId);
@@ -3157,6 +3284,8 @@ async function findProgramsByTarget(client, config, input) {
3157
3284
  total_targets_checked: targets.length
3158
3285
  }));
3159
3286
  }
3287
+ }
3288
+ function programsByTargetResult(input, warnings, sourceRequests, matches, budget, options) {
3160
3289
  return stripUndefined({
3161
3290
  request_id: randomUUID(),
3162
3291
  source_requests: sourceRequests,
@@ -3166,7 +3295,7 @@ async function findProgramsByTarget(client, config, input) {
3166
3295
  match_mode: input.match_mode,
3167
3296
  platforms: input.platforms.length > 0 ? input.platforms : undefined,
3168
3297
  tags: input.tags.length > 0 ? input.tags : undefined,
3169
- source: "sampled_program_targets",
3298
+ source: options.source,
3170
3299
  include_out_of_scope: input.include_out_of_scope,
3171
3300
  include_ineligible: input.include_ineligible,
3172
3301
  target_list_mode: input.target_list_mode
@@ -3177,63 +3306,79 @@ async function findProgramsByTarget(client, config, input) {
3177
3306
  used: budget.initial - budget.remaining,
3178
3307
  remaining: budget.remaining
3179
3308
  },
3180
- ranking_scope: {
3181
- mode: "sampled",
3182
- max_pages_scanned_per_source: maxPages,
3183
- upstream_total_pages: collected.totalPagesBySource,
3184
- possibly_incomplete: isRankingPossiblyIncomplete(collected.totalPagesBySource, maxPages)
3185
- },
3186
- meta: {
3187
- returned: matches.length,
3188
- programs_scanned: collected.programsScanned,
3189
- target_lists_checked: sourceRequests.filter((request) => request.source === "program_targets").length
3190
- }
3309
+ ranking_scope: options.rankingScope,
3310
+ meta: options.meta
3191
3311
  });
3192
3312
  }
3193
3313
  async function searchProgramsByTargetApi(client, config, input, warnings) {
3194
- let api;
3195
- try {
3196
- api = await client.searchTargets({
3197
- q: input.target_query,
3198
- match_mode: upstreamTargetMatchMode(input.match_mode),
3199
- platforms: input.platforms,
3200
- tags: input.tags,
3201
- include_out_of_scope: input.include_out_of_scope,
3202
- include_ineligible: input.include_ineligible,
3203
- page: 1,
3204
- page_size: Math.max(input.max_results * input.max_targets_per_program, input.max_results)
3205
- });
3206
- }
3207
- catch (error) {
3208
- if (error instanceof BBRadarApiError && (error.status === 404 || error.status === 405)) {
3209
- warnings.push("Direct target search API is unavailable; fell back to sampled program target lists.");
3210
- return undefined;
3211
- }
3212
- throw error;
3213
- }
3214
- const data = readObject(api.data);
3215
- const rawMatches = readTargetSearchRows(data);
3314
+ const searchQueries = targetApiSearchQueries(input.target_query);
3216
3315
  const grouped = new Map();
3217
- for (const row of rawMatches) {
3218
- const program = addProgramResourceLinks(sanitizeProgram(readObject(row.program) ?? readObject(readObject(row.target)?.program) ?? row, config.webBaseUrl));
3219
- const programId = stringField(program, "id") ?? stringField(row, "program_id");
3220
- const target = sanitizeTarget(readObject(row.target) ?? row);
3221
- if (!programId || !targetIdentifier(target)) {
3222
- continue;
3316
+ const sourceRequests = [];
3317
+ let rawMatchesScanned = 0;
3318
+ let lastMeta;
3319
+ for (const searchQuery of searchQueries) {
3320
+ let api;
3321
+ try {
3322
+ api = await client.searchTargets({
3323
+ q: searchQuery,
3324
+ match_mode: upstreamTargetMatchMode(input.match_mode),
3325
+ platforms: input.platforms,
3326
+ tags: input.tags,
3327
+ include_out_of_scope: input.include_out_of_scope,
3328
+ include_ineligible: input.include_ineligible,
3329
+ page: 1,
3330
+ page_size: Math.max(input.max_results * input.max_targets_per_program, input.max_results)
3331
+ });
3223
3332
  }
3224
- if (!targetHasAllowedScope(target, {
3225
- include_out_of_scope: input.include_out_of_scope,
3226
- include_ineligible: input.include_ineligible,
3227
- strict_scope_filter: false
3228
- })) {
3229
- continue;
3333
+ catch (error) {
3334
+ if (error instanceof BBRadarApiError && (error.status === 404 || error.status === 405)) {
3335
+ warnings.push("Direct target search API is unavailable; fell back to sampled program target lists.");
3336
+ return undefined;
3337
+ }
3338
+ throw error;
3230
3339
  }
3231
- if (!targetMatchesQuery(target, input.target_query, input.match_mode)) {
3232
- continue;
3340
+ const data = readObject(api.data);
3341
+ const rawMatches = readTargetSearchRows(data);
3342
+ rawMatchesScanned += rawMatches.length;
3343
+ lastMeta = readObject(sanitizeJson(data?.meta)) ?? lastMeta;
3344
+ sourceRequests.push({
3345
+ source: "target_search",
3346
+ query: searchQuery,
3347
+ api_match_mode: upstreamTargetMatchMode(input.match_mode),
3348
+ request_id: api.requestId,
3349
+ upstream_request_id: api.upstreamRequestId,
3350
+ ...apiSourceMetadata(api)
3351
+ });
3352
+ for (const row of rawMatches) {
3353
+ const rowProgram = sanitizeProgram(readObject(row.program) ?? row, config.webBaseUrl);
3354
+ const rowProgramId = stringField(rowProgram, "id") ?? stringField(row, "program_id");
3355
+ const targets = targetSearchRowTargets(row);
3356
+ for (const targetSource of targets) {
3357
+ const target = sanitizeTarget(targetSource);
3358
+ const nestedProgram = sanitizeProgram(readObject(targetSource.program), config.webBaseUrl);
3359
+ const program = stringField(nestedProgram, "id") ? nestedProgram : rowProgram;
3360
+ const programId = stringField(program, "id") ?? rowProgramId ?? stringField(targetSource, "program_id");
3361
+ if (!programId || !targetIdentifier(target)) {
3362
+ continue;
3363
+ }
3364
+ if (!targetHasAllowedScope(target, {
3365
+ include_out_of_scope: input.include_out_of_scope,
3366
+ include_ineligible: input.include_ineligible,
3367
+ strict_scope_filter: false
3368
+ })) {
3369
+ continue;
3370
+ }
3371
+ if (!targetMatchesQuery(target, input.target_query, input.match_mode)) {
3372
+ continue;
3373
+ }
3374
+ const existing = grouped.get(programId) ?? { program: addProgramResourceLinks({ ...program, id: programId }), targets: [] };
3375
+ existing.targets.push(target);
3376
+ grouped.set(programId, existing);
3377
+ }
3378
+ }
3379
+ if (grouped.size >= input.max_results) {
3380
+ break;
3233
3381
  }
3234
- const existing = grouped.get(programId) ?? { program: { ...program, id: programId }, targets: [] };
3235
- existing.targets.push(target);
3236
- grouped.set(programId, existing);
3237
3382
  }
3238
3383
  const matches = [...grouped.values()].slice(0, input.max_results).map((entry) => stripUndefined({
3239
3384
  program: formatProgram(entry.program, "compact"),
@@ -3245,21 +3390,13 @@ async function searchProgramsByTargetApi(client, config, input, warnings) {
3245
3390
  warnings.push("Direct target search returned no smart matches; fell back to sampled program target lists.");
3246
3391
  return undefined;
3247
3392
  }
3248
- const sanitizedMeta = readObject(sanitizeJson(data?.meta));
3249
3393
  return stripUndefined({
3250
3394
  request_id: randomUUID(),
3251
- source_requests: [
3252
- {
3253
- source: "target_search",
3254
- api_match_mode: upstreamTargetMatchMode(input.match_mode),
3255
- request_id: api.requestId,
3256
- upstream_request_id: api.upstreamRequestId,
3257
- ...apiSourceMetadata(api)
3258
- }
3259
- ],
3395
+ source_requests: sourceRequests,
3260
3396
  warnings: warnings.length > 0 ? warnings : undefined,
3261
3397
  filters: {
3262
3398
  target_query: input.target_query,
3399
+ target_search_queries: searchQueries.length > 1 ? searchQueries : undefined,
3263
3400
  match_mode: input.match_mode,
3264
3401
  platforms: input.platforms.length > 0 ? input.platforms : undefined,
3265
3402
  tags: input.tags.length > 0 ? input.tags : undefined,
@@ -3271,19 +3408,52 @@ async function searchProgramsByTargetApi(client, config, input, warnings) {
3271
3408
  matches,
3272
3409
  programs: matches.map((match) => readObject(match.program)).filter((program) => program !== undefined),
3273
3410
  meta: stripUndefined({
3274
- ...(sanitizedMeta ?? {}),
3411
+ ...(lastMeta ?? {}),
3275
3412
  returned: matches.length,
3276
- target_search_rows_scanned: rawMatches.length
3413
+ target_search_rows_scanned: rawMatchesScanned
3277
3414
  })
3278
3415
  });
3279
3416
  }
3280
3417
  function readTargetSearchRows(data) {
3281
- return [data?.results, data?.matches, data?.targets]
3418
+ return [data?.results, data?.matches, data?.programs, data?.targets]
3282
3419
  .map(readArray)
3283
3420
  .find((rows) => rows.length > 0)
3284
3421
  ?.map((row) => readObject(row))
3285
3422
  .filter((row) => row !== undefined) ?? [];
3286
3423
  }
3424
+ function targetSearchRowTargets(row) {
3425
+ const directTarget = readObject(row.target);
3426
+ const arrays = [
3427
+ row.targets,
3428
+ row.matched_targets,
3429
+ row.matching_targets,
3430
+ row.target_matches,
3431
+ row.scope
3432
+ ]
3433
+ .flatMap(readArray)
3434
+ .map(targetSearchEntryToTarget)
3435
+ .filter((target) => target !== undefined);
3436
+ if (directTarget) {
3437
+ return [directTarget, ...arrays];
3438
+ }
3439
+ if (arrays.length > 0) {
3440
+ return arrays;
3441
+ }
3442
+ if (targetIdentifier(row)) {
3443
+ return [row];
3444
+ }
3445
+ return [];
3446
+ }
3447
+ function targetSearchEntryToTarget(entry) {
3448
+ if (typeof entry === "string") {
3449
+ return { identifier: entry };
3450
+ }
3451
+ const object = readObject(entry);
3452
+ if (!object) {
3453
+ return undefined;
3454
+ }
3455
+ return readObject(object.target) ?? object;
3456
+ }
3287
3457
  function rewardThresholdFilters(minReward, mode) {
3288
3458
  return {
3289
3459
  min_bounty_min: minReward !== undefined && mode === "min_at_least" ? minReward : undefined,
@@ -5199,7 +5369,8 @@ function programResolutionMatch(candidate, query) {
5199
5369
  };
5200
5370
  }
5201
5371
  function hasHighConfidenceProgramResolutionMatch(programs, webBaseUrl, query) {
5202
- return programs.some((program) => programResolutionMatch(toProgramCandidate(program, webBaseUrl), query).score >= 85);
5372
+ const threshold = looksLikeDirectProgramHandleQuery(query) ? 95 : 85;
5373
+ return programs.some((program) => programResolutionMatch(toProgramCandidate(program, webBaseUrl), query).score >= threshold);
5203
5374
  }
5204
5375
  function bestProgramResolutionScore(programs, webBaseUrl, query) {
5205
5376
  return programs.reduce((best, program) => Math.max(best, programResolutionMatch(toProgramCandidate(program, webBaseUrl), query).score), 0);
@@ -5210,6 +5381,7 @@ function programResolutionSearchQueries(query) {
5210
5381
  function programResolutionQueryVariants(query) {
5211
5382
  const normalized = normalizeTag(query);
5212
5383
  const meaningfulTokens = [...meaningfulProgramQueryTokens(query)];
5384
+ const searchTokens = meaningfulTokens.filter((token) => token.length >= 3);
5213
5385
  const meaningfulPhrase = meaningfulTokens.join(" ");
5214
5386
  const singleTokenNameVariants = meaningfulTokens.length === 1
5215
5387
  ? [
@@ -5223,7 +5395,7 @@ function programResolutionQueryVariants(query) {
5223
5395
  normalized,
5224
5396
  meaningfulPhrase,
5225
5397
  meaningfulTokens.join("-"),
5226
- ...meaningfulTokens,
5398
+ ...searchTokens,
5227
5399
  ...singleTokenNameVariants
5228
5400
  ];
5229
5401
  return uniqueStrings(values.filter((value) => value.length >= 2));
@@ -5451,9 +5623,30 @@ async function collectProgramsForResolution(client, options) {
5451
5623
  const collections = [];
5452
5624
  const queryCollections = [];
5453
5625
  const queries = options.searchQueries.length > 0 ? options.searchQueries : [undefined];
5626
+ const primaryDirectPlatforms = options.platforms.length > 0 ? options.platforms : PRIMARY_PROGRAM_ID_PLATFORM_GUESSES;
5627
+ const primaryDirectProgramIds = new Set(directProgramIdCandidates(options.query, primaryDirectPlatforms, { maxHandleVariants: 1 }));
5628
+ if (primaryDirectProgramIds.size > 0 && options.budget.remaining > 0) {
5629
+ const primaryDirectHandles = await collectDirectHandlePrograms(client, {
5630
+ query: options.query,
5631
+ platforms: primaryDirectPlatforms,
5632
+ budget: options.budget,
5633
+ warnings: options.warnings,
5634
+ source: "program_id_handle_guess",
5635
+ maxProgramIds: MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS,
5636
+ maxHandleVariants: 1,
5637
+ acceptEmptyTargetsAsPrograms: true,
5638
+ stopOnFirstMatch: true
5639
+ });
5640
+ if (primaryDirectHandles.programs.length > 0 || primaryDirectHandles.requestsScanned > 0) {
5641
+ collections.push(primaryDirectHandles);
5642
+ }
5643
+ if (primaryDirectHandles.programs.length > 0 && options.isHighConfidenceMatch?.(mergeProgramCollections(collections).programs)) {
5644
+ return mergeProgramCollections(collections);
5645
+ }
5646
+ }
5454
5647
  for (const search of queries) {
5455
5648
  if (options.budget.remaining <= 0) {
5456
- addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all program name search variants could be checked.");
5649
+ addUniqueWarning(options.warnings, "Program name search stopped at the request budget; pass a higher upstream_request_budget for a deeper resolution attempt.");
5457
5650
  break;
5458
5651
  }
5459
5652
  const collection = await collectPrograms(client, {
@@ -5472,6 +5665,25 @@ async function collectProgramsForResolution(client, options) {
5472
5665
  return mergeProgramCollections(collections);
5473
5666
  }
5474
5667
  }
5668
+ if (options.budget.remaining > 0) {
5669
+ const directHandles = await collectDirectHandlePrograms(client, {
5670
+ query: options.query,
5671
+ platforms: options.platforms,
5672
+ budget: options.budget,
5673
+ warnings: options.warnings,
5674
+ source: "program_id_handle_guess",
5675
+ skipProgramIds: primaryDirectProgramIds,
5676
+ maxHandleVariants: 4,
5677
+ acceptEmptyTargetsAsPrograms: true,
5678
+ stopOnFirstMatch: true
5679
+ });
5680
+ if (directHandles.programs.length > 0 || directHandles.requestsScanned > 0) {
5681
+ collections.push(directHandles);
5682
+ }
5683
+ if (directHandles.programs.length > 0 && options.isHighConfidenceMatch?.(mergeProgramCollections(collections).programs)) {
5684
+ return mergeProgramCollections(collections);
5685
+ }
5686
+ }
5475
5687
  if (options.max_pages > 1) {
5476
5688
  const search = bestResolutionDeepSearch(queryCollections, options.scorePrograms) ?? queries[0];
5477
5689
  if (options.budget.remaining <= 0) {
@@ -5532,6 +5744,157 @@ function mergeProgramCollections(collections) {
5532
5744
  totalPagesBySource
5533
5745
  };
5534
5746
  }
5747
+ async function collectDirectHandlePrograms(client, options) {
5748
+ const programIds = directProgramIdCandidates(options.query, options.platforms, { maxHandleVariants: options.maxHandleVariants })
5749
+ .filter((programId) => !options.skipProgramIds?.has(programId))
5750
+ .slice(0, options.maxProgramIds ?? MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS);
5751
+ const programs = [];
5752
+ const sourceRequests = [];
5753
+ let requestsScanned = 0;
5754
+ for (const programId of programIds) {
5755
+ requestsScanned += 1;
5756
+ try {
5757
+ const targetsApi = await client.getProgramTargets(programId);
5758
+ const targetsData = readObject(targetsApi.data);
5759
+ const hasTargetsArray = Array.isArray(targetsData?.targets);
5760
+ const targets = readArray(targetsData?.targets).map(sanitizeTarget);
5761
+ const programEndpointExists = targets.length > 0 || (options.acceptEmptyTargetsAsPrograms === true && hasTargetsArray);
5762
+ if (programEndpointExists) {
5763
+ programs.push(syntheticProgramFromProgramId(programId, targets));
5764
+ }
5765
+ sourceRequests.push({
5766
+ source: options.source,
5767
+ endpoint: "program_targets",
5768
+ program_id: programId,
5769
+ request_id: targetsApi.requestId,
5770
+ upstream_request_id: targetsApi.upstreamRequestId,
5771
+ ...apiSourceMetadata(targetsApi)
5772
+ });
5773
+ if (programEndpointExists && options.stopOnFirstMatch) {
5774
+ break;
5775
+ }
5776
+ }
5777
+ catch (error) {
5778
+ if (error instanceof BBRadarApiError && error.status === 404) {
5779
+ sourceRequests.push({
5780
+ source: options.source,
5781
+ endpoint: "program_targets",
5782
+ program_id: programId,
5783
+ request_id: error.requestId,
5784
+ upstream_request_id: error.upstreamRequestId,
5785
+ status: error.status
5786
+ });
5787
+ continue;
5788
+ }
5789
+ throw error;
5790
+ }
5791
+ }
5792
+ return {
5793
+ programs,
5794
+ sourceRequests,
5795
+ requestsScanned,
5796
+ programsScanned: programs.length,
5797
+ totalPagesBySource: programs.length > 0 ? { [options.source]: 1 } : {}
5798
+ };
5799
+ }
5800
+ function syntheticProgramFromProgramId(programId, targets) {
5801
+ const separatorIndex = programId.indexOf(":");
5802
+ const platform = separatorIndex >= 0 ? programId.slice(0, separatorIndex) : undefined;
5803
+ const handle = separatorIndex >= 0 ? programId.slice(separatorIndex + 1) : programId;
5804
+ const displayHandle = handle.split("/").filter(Boolean).at(-1) ?? handle;
5805
+ const scopeTags = uniqueStrings(targets.flatMap((target) => readStringArrayField(target, "scope_tags")));
5806
+ return stripUndefined({
5807
+ id: programId,
5808
+ handle,
5809
+ name: titleizeProgramHandle(displayHandle),
5810
+ platform,
5811
+ scope_tags: scopeTags,
5812
+ target_counts: {
5813
+ total: targets.length,
5814
+ in_scope: targets.filter((target) => target.in_scope === true).length,
5815
+ out_of_scope: targets.filter((target) => target.in_scope === false).length,
5816
+ eligible_for_bounty: targets.filter((target) => target.eligible_for_bounty === true).length,
5817
+ wildcard: targets.filter(targetIsWildcard).length
5818
+ }
5819
+ });
5820
+ }
5821
+ function titleizeProgramHandle(handle) {
5822
+ return handle
5823
+ .split(/[-_.\s]+/)
5824
+ .filter(Boolean)
5825
+ .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
5826
+ .join(" ");
5827
+ }
5828
+ function directProgramIdCandidates(query, platforms, options = {}) {
5829
+ const platformGuesses = (platforms.length > 0 ? platforms : PROGRAM_ID_PLATFORM_GUESSES).map(canonicalProgramIdPlatform);
5830
+ const handles = directProgramHandleVariants(query).slice(0, options.maxHandleVariants ?? Number.POSITIVE_INFINITY);
5831
+ return uniqueStrings(handles.flatMap((handle) => platformGuesses.flatMap((platform) => normalizeTag(platform) === "bugcrowd"
5832
+ ? [`${platform}:engagements/${handle}`, `${platform}:${handle}`]
5833
+ : [`${platform}:${handle}`]))).slice(0, MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS);
5834
+ }
5835
+ function canonicalProgramIdPlatform(platform) {
5836
+ const normalized = normalizeTag(platform).replace(/[^a-z0-9]/g, "");
5837
+ const knownPlatforms = {
5838
+ hackerone: "HackerOne",
5839
+ bugcrowd: "BugCrowd",
5840
+ intigriti: "Intigriti",
5841
+ hackenproof: "HackenProof",
5842
+ yeswehack: "YesWeHack",
5843
+ immunefi: "Immunefi",
5844
+ cantina: "Cantina",
5845
+ code4rena: "Code4rena",
5846
+ huntr: "Huntr",
5847
+ gobugfree: "GObugfree",
5848
+ bugbountyswitzerland: "Bug Bounty Switzerland",
5849
+ inspectiv: "Inspectiv",
5850
+ compasssecurity: "Compass Security",
5851
+ bugrap: "BugRap",
5852
+ issuehunt: "IssueHunt",
5853
+ patchday: "PatchDay",
5854
+ remedy: "Remedy",
5855
+ standoff365: "Standoff365",
5856
+ sherlock: "Sherlock",
5857
+ codehawks: "CodeHawks"
5858
+ };
5859
+ return knownPlatforms[normalized] ?? platform.trim();
5860
+ }
5861
+ function directProgramHandleVariants(query) {
5862
+ const handleText = query.includes(":") ? query.slice(query.indexOf(":") + 1) : query;
5863
+ const normalized = normalizeTargetIdentifierForMatch(handleText);
5864
+ const literal = directProgramLiteralHandleVariant(handleText);
5865
+ const dashed = normalized.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5866
+ const compact = dashed.replace(/-/g, "");
5867
+ const tokens = [...meaningfulProgramQueryTokens(handleText)];
5868
+ const brandTokens = targetBrandTokens(handleText);
5869
+ const literalVariants = [literal, dashed, compact, tokens.join("-"), tokens.join("")];
5870
+ const values = queryLooksLikeTargetIdentifier(handleText) ? [...brandTokens, ...literalVariants] : [...literalVariants, ...brandTokens];
5871
+ return uniqueStrings(values).filter(isDirectProgramHandleVariant);
5872
+ }
5873
+ function looksLikeDirectProgramHandleQuery(query) {
5874
+ const handleText = query.includes(":") ? query.slice(query.indexOf(":") + 1) : query;
5875
+ return isDirectProgramHandleVariant(directProgramLiteralHandleVariant(handleText));
5876
+ }
5877
+ function directProgramLiteralHandleVariant(value) {
5878
+ const normalized = value
5879
+ .trim()
5880
+ .replace(/\\/g, "/")
5881
+ .replace(/^['"`<]+|['"`>]+$/g, "")
5882
+ .replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
5883
+ .replace(/^www\./i, "");
5884
+ const queryStart = normalized.search(/[?#]/);
5885
+ return (queryStart >= 0 ? normalized.slice(0, queryStart) : normalized).replace(/\/+$/, "");
5886
+ }
5887
+ function isDirectProgramHandleVariant(value) {
5888
+ if (!value) {
5889
+ return false;
5890
+ }
5891
+ if (value.length < 2 || value.length > 150 || /[\u0000-\u001F\u007F?#:]/.test(value)) {
5892
+ return false;
5893
+ }
5894
+ return value
5895
+ .split("/")
5896
+ .every((segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/^[\s.]+$/.test(segment));
5897
+ }
5535
5898
  async function collectProgramSources(client, sources, options) {
5536
5899
  const pages = [];
5537
5900
  await mapWithConcurrency(sources.map((source, sourceIndex) => ({ source, sourceIndex })), PROGRAM_COLLECTION_CONCURRENCY, async ({ source, sourceIndex }) => {
@@ -6299,13 +6662,13 @@ function targetMatchesQuery(target, query, mode) {
6299
6662
  if (mode === "wildcard") {
6300
6663
  return profile.values.some((value) => queryValues.some((queryValue) => targetWildcardMatch(value, queryValue)));
6301
6664
  }
6302
- const containsMatch = profile.values.some((value) => queryValues.some((queryValue) => value.includes(queryValue) || queryValue.includes(value)));
6665
+ const containsMatch = profile.values.some((value) => queryValues.some((queryValue) => targetContainsMatch(value, queryValue)));
6303
6666
  if (mode === "contains") {
6304
6667
  return containsMatch;
6305
6668
  }
6306
6669
  return (containsMatch ||
6307
6670
  profile.values.some((value) => queryValues.some((queryValue) => targetSuffixMatch(value, queryValue) || targetWildcardMatch(value, queryValue))) ||
6308
- targetTokensMatch(profile.tokens, query));
6671
+ (!queryLooksLikeTargetIdentifier(query) && targetTokensMatch(profile.tokens, query)));
6309
6672
  }
6310
6673
  function upstreamTargetMatchMode(mode) {
6311
6674
  return mode === "smart" ? "contains" : mode;
@@ -6343,7 +6706,24 @@ function targetSearchProfile(target) {
6343
6706
  };
6344
6707
  }
6345
6708
  function targetQueryVariants(query) {
6346
- return targetStringVariants(query);
6709
+ const variants = targetStringVariants(query);
6710
+ if (!queryLooksLikeTargetIdentifier(query)) {
6711
+ return variants;
6712
+ }
6713
+ return variants.filter(targetValueCanContainQuery);
6714
+ }
6715
+ function targetApiSearchQueries(query) {
6716
+ const normalized = normalizeTargetIdentifierForMatch(query);
6717
+ const brandDomains = targetBrandTokens(query).flatMap((token) => [`${token}.com`, `*.${token}.com`]);
6718
+ return uniqueStrings([
6719
+ normalized,
6720
+ ...targetQueryVariants(query),
6721
+ ...brandDomains
6722
+ ].filter((entry) => entry !== undefined && entry.length >= 2)).slice(0, MAX_RESOLVE_SEARCH_QUERIES);
6723
+ }
6724
+ function queryLooksLikeTargetIdentifier(query) {
6725
+ const normalized = normalizeTargetIdentifierForMatch(query);
6726
+ return /[./*:]/.test(normalized);
6347
6727
  }
6348
6728
  function targetStringVariants(value) {
6349
6729
  const normalized = normalizeTargetIdentifierForMatch(value);
@@ -6373,6 +6753,15 @@ function targetSuffixMatch(value, query) {
6373
6753
  function targetWildcardMatch(value, query) {
6374
6754
  return wildcardTargetMatch(query, value) || wildcardTargetMatch(value, query);
6375
6755
  }
6756
+ function targetContainsMatch(value, query) {
6757
+ if (value.includes(query)) {
6758
+ return true;
6759
+ }
6760
+ return targetValueCanContainQuery(value) && query.includes(value);
6761
+ }
6762
+ function targetValueCanContainQuery(value) {
6763
+ return value.includes(".") || value.includes("/") || value.includes("*") || value.includes(":");
6764
+ }
6376
6765
  function targetTokensMatch(targetTokens, query) {
6377
6766
  const queryTokens = [...targetTokenSet(query)];
6378
6767
  if (queryTokens.length === 0) {
@@ -6389,6 +6778,17 @@ function targetTokenSet(value) {
6389
6778
  const meaningfulTokens = tokens.filter((token) => !TARGET_QUERY_STOP_WORDS.has(token));
6390
6779
  return new Set(meaningfulTokens.length > 0 ? meaningfulTokens : tokens);
6391
6780
  }
6781
+ function targetBrandTokens(value) {
6782
+ const normalized = stripLeadingTargetWildcard(normalizeTargetIdentifierForMatch(value));
6783
+ const host = targetHostVariant(normalized) ?? (normalized.includes(".") && !normalized.includes("/") ? normalized : undefined);
6784
+ const hostBrand = host
6785
+ ?.split(".")
6786
+ .filter(Boolean)
6787
+ .slice(-2, -1)
6788
+ .find((token) => token.length >= 3 && !TARGET_QUERY_STOP_WORDS.has(token));
6789
+ const textTokens = [...targetTokenSet(normalized)].filter((token) => token.length >= 4);
6790
+ return uniqueStrings([hostBrand, ...textTokens].filter((entry) => entry !== undefined));
6791
+ }
6392
6792
  function normalizeTargetIdentifierForMatch(value) {
6393
6793
  const normalized = value
6394
6794
  .trim()