@bbradar/mcp 0.2.0 → 0.2.1
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 +4 -3
- package/dist/server.js +343 -28
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ Large target and change collections return at most 50 inline records by default.
|
|
|
131
131
|
|
|
132
132
|
## Performance Tuning
|
|
133
133
|
|
|
134
|
-
The defaults expose the
|
|
134
|
+
The defaults expose the 17-tool core surface and compact text results while enabling bounded concurrency, positive response caching, in-flight request coalescing, stale-while-revalidate program metadata, endpoint circuit breakers, and short negative caching for repeated 404/405 lookups. Set the tool profile to `full` when all specialized tools are needed.
|
|
135
135
|
|
|
136
136
|
| Environment variable | Default | Purpose |
|
|
137
137
|
| --- | ---: | --- |
|
|
@@ -141,7 +141,7 @@ The defaults expose the 16-tool core surface and compact text results while enab
|
|
|
141
141
|
| `BBRADAR_MCP_MAX_CONCURRENT_REQUESTS` | `6` | Maximum simultaneous upstream BBRadar API requests. |
|
|
142
142
|
| `BBRADAR_MCP_REQUEST_TIMEOUT_SECONDS` | `30` | Timeout for each active upstream request; queue time is excluded. |
|
|
143
143
|
| `BBRADAR_MCP_COMPACT_TOOL_TEXT` | `true` | Avoid duplicating full results for clients that consume MCP `structuredContent`. Set to `false` for legacy text-only clients. |
|
|
144
|
-
| `BBRADAR_MCP_TOOL_PROFILE` | `core` | Compact
|
|
144
|
+
| `BBRADAR_MCP_TOOL_PROFILE` | `core` | Compact 17-tool discovery surface. Set to `full` for every specialized tool. |
|
|
145
145
|
| `BBRADAR_MCP_PROGRAM_STALE_WHILE_REVALIDATE_SECONDS` | `300` | Maximum stale window for exact program metadata while one background refresh runs. |
|
|
146
146
|
| `BBRADAR_MCP_CIRCUIT_BREAKER_FAILURE_THRESHOLD` | `3` | Consecutive retryable endpoint failures before its circuit opens. |
|
|
147
147
|
| `BBRADAR_MCP_CIRCUIT_BREAKER_COOLDOWN_SECONDS` | `30` | Endpoint circuit cooldown before a new request is admitted. |
|
|
@@ -164,7 +164,7 @@ Run `npm run benchmark` for the deterministic local performance benchmark. It re
|
|
|
164
164
|
|
|
165
165
|
Use `find_programs` as the generic ranked path for combinations of platform, reward, opportunity, scope breadth, technology, competition, and freshness signals. It supports both inclusion and exclusion through `platforms`, `exclude_platforms`, `web3`, `exclude_web3`, and the remaining scope/reward/activity filters. When a user describes an exclusion without naming exact platform values, call `list_filters` first and reuse the observed values. If an intentionally strict combination returns no candidates, relax one local threshold at a time; the upstream program pages are cached, so these fallback passes normally add no API requests.
|
|
166
166
|
|
|
167
|
-
For repository-aware hunting,
|
|
167
|
+
For repository-aware hunting, use `find_repository_hunt_targets`. It fetches repository events and program/reward candidates concurrently inside one MCP call, joins them by program id, filters scored targets, removes non-requested associated platforms, and ranks the final matches by a balanced, reward-first, target-first, or recency-first strategy. The inputs remain generic across platforms and technology stacks, so the AI client does not need to plan and wait for a long sequence of separate calls.
|
|
168
168
|
|
|
169
169
|
### Optimized wildcard discovery
|
|
170
170
|
|
|
@@ -192,6 +192,7 @@ This path uses one recent-changes request. Use `find_wildcard_programs` for a ra
|
|
|
192
192
|
- `get_program_targets`
|
|
193
193
|
- `get_recent_changes`
|
|
194
194
|
- `get_repository_changes`
|
|
195
|
+
- `find_repository_hunt_targets`
|
|
195
196
|
- `get_opportunities`
|
|
196
197
|
- `find_programs`
|
|
197
198
|
- `find_wildcard_programs`
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { NumericCursorStore } from "./cursorStore.js";
|
|
|
6
6
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
7
7
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
8
8
|
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
-
const SERVER_VERSION = "0.2.
|
|
9
|
+
const SERVER_VERSION = "0.2.1";
|
|
10
10
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
11
11
|
const MAX_TARGET_EXPORT = 500;
|
|
12
12
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -50,6 +50,7 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
50
50
|
"get_program_targets",
|
|
51
51
|
"get_recent_changes",
|
|
52
52
|
"get_repository_changes",
|
|
53
|
+
"find_repository_hunt_targets",
|
|
53
54
|
"get_latest_added_targets",
|
|
54
55
|
"get_opportunities",
|
|
55
56
|
"find_programs",
|
|
@@ -61,6 +62,7 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
61
62
|
]);
|
|
62
63
|
const TOOL_ENDPOINT_REQUIREMENTS = {
|
|
63
64
|
get_repository_changes: "repository_changes",
|
|
65
|
+
find_repository_hunt_targets: "repository_changes",
|
|
64
66
|
export_targets: "target_export",
|
|
65
67
|
get_opportunities: "opportunities"
|
|
66
68
|
};
|
|
@@ -682,7 +684,7 @@ export function createBbradarServer(client, config) {
|
|
|
682
684
|
}));
|
|
683
685
|
registerTool("get_repository_changes", {
|
|
684
686
|
title: "Get Repository Changes",
|
|
685
|
-
description: "Read-only repository push/change feed. Defaults to lightweight program summaries; use after_id for efficient incremental polling.
|
|
687
|
+
description: "Read-only repository push/change feed for direct inspection or polling. Defaults to lightweight program summaries; use after_id for efficient incremental polling. Use find_repository_hunt_targets instead when reward or target-quality ranking is also required.",
|
|
686
688
|
inputSchema: {
|
|
687
689
|
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns events after this id in ascending order."),
|
|
688
690
|
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
@@ -739,6 +741,51 @@ export function createBbradarServer(client, config) {
|
|
|
739
741
|
source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
|
|
740
742
|
}), "repository_changes", "repository changes", continuationStore, config);
|
|
741
743
|
}));
|
|
744
|
+
registerTool("find_repository_hunt_targets", {
|
|
745
|
+
title: "Find Repository-Aware Hunt Targets",
|
|
746
|
+
description: "Fast one-call path for requests that combine recent repository activity with program rewards and target quality. Fetches program rankings and repository changes in parallel, joins them locally, and works with arbitrary platforms, Web2/Web3 modes, reward thresholds, target types, languages, and freshness criteria.",
|
|
747
|
+
inputSchema: {
|
|
748
|
+
platforms: platformListSchema(),
|
|
749
|
+
exclude_platforms: stringListSchema("Platforms to exclude from final matches."),
|
|
750
|
+
tags: tagListSchema(),
|
|
751
|
+
scope_tags: scopeTagListSchema(),
|
|
752
|
+
target_types: targetTypeListSchema(),
|
|
753
|
+
language_tags: languageTagListSchema(),
|
|
754
|
+
surface_mode: z.enum(["any", "web2", "web3"]).default("any"),
|
|
755
|
+
opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
|
|
756
|
+
min_bounty_max: z.number().int().min(0).optional(),
|
|
757
|
+
require_bounty: z.boolean().default(true),
|
|
758
|
+
max_public_report_count: z.number().int().min(0).optional(),
|
|
759
|
+
include_unknown_report_count: z.boolean().default(true),
|
|
760
|
+
min_eligible_targets: z.number().int().min(0).default(1),
|
|
761
|
+
min_total_targets: z.number().int().min(0).optional(),
|
|
762
|
+
min_wildcards: z.number().int().min(0).max(1000).default(0),
|
|
763
|
+
min_added_7d: z.number().int().min(0).optional(),
|
|
764
|
+
changed_since: isoDateTimeSchema.optional(),
|
|
765
|
+
free_submission_only: z.boolean().default(false),
|
|
766
|
+
min_target_score: z.number().int().min(0).max(100).default(60),
|
|
767
|
+
target_opportunity_labels: z.array(targetOpportunityLabelSchema).max(4).default([]),
|
|
768
|
+
competition_risks: z.array(competitionRiskSchema).max(4).default([]),
|
|
769
|
+
max_program_pages: z.number().int().min(1).max(MAX_FIND_PROGRAM_PAGES).default(3),
|
|
770
|
+
max_program_candidates: z.number().int().min(1).max(MAX_FIND_PROGRAM_RESULTS).default(MAX_FIND_PROGRAM_RESULTS),
|
|
771
|
+
repository_page_size: z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(100, maxRepositoryChangesPageSize)),
|
|
772
|
+
max_results: z.number().int().min(1).max(25).default(10),
|
|
773
|
+
max_targets_per_result: z.number().int().min(1).max(20).default(5),
|
|
774
|
+
ranking_mode: z.enum(["balanced", "highest_reward", "best_target", "newest"]).default("balanced"),
|
|
775
|
+
output_mode: repositoryChangeListModeSchema.default("compact"),
|
|
776
|
+
upstream_request_budget: z.number().int().min(2).max(MAX_FIND_UPSTREAM_REQUEST_BUDGET).default(10)
|
|
777
|
+
},
|
|
778
|
+
outputSchema: {
|
|
779
|
+
...apiEnvelopeOutputShape(),
|
|
780
|
+
matches: z.array(jsonRecordSchema()).optional(),
|
|
781
|
+
programs: z.array(jsonRecordSchema()).optional(),
|
|
782
|
+
filters: jsonRecordSchema().optional(),
|
|
783
|
+
upstream_budget: jsonRecordSchema().optional(),
|
|
784
|
+
ranking_scope: jsonRecordSchema().optional(),
|
|
785
|
+
meta: jsonRecordSchema().optional()
|
|
786
|
+
},
|
|
787
|
+
annotations: readOnlyAnnotations
|
|
788
|
+
}, (args) => runTool("find_repository_hunt_targets", config, rateLimiter, async () => findRepositoryHuntTargets(client, config, args)));
|
|
742
789
|
registerTool("get_latest_added_targets", {
|
|
743
790
|
title: "Get Latest Added Targets",
|
|
744
791
|
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.",
|
|
@@ -1839,6 +1886,12 @@ function bbradarGuide(topic, profile, client) {
|
|
|
1839
1886
|
minimum_arguments: {},
|
|
1840
1887
|
useful_arguments: ["platforms", "target_types", "language_tags", "min_bounty_max", "max_public_report_count", "fresh_launch_days", "has_api_scope"]
|
|
1841
1888
|
},
|
|
1889
|
+
{
|
|
1890
|
+
tool: "find_repository_hunt_targets",
|
|
1891
|
+
use_when: "The request combines repository activity with reward, target-quality, platform, technology, or freshness filters.",
|
|
1892
|
+
minimum_arguments: {},
|
|
1893
|
+
useful_arguments: ["platforms", "exclude_platforms", "surface_mode", "min_bounty_max", "min_target_score", "changed_since", "ranking_mode"]
|
|
1894
|
+
},
|
|
1842
1895
|
{
|
|
1843
1896
|
tool: "run_program_name_action",
|
|
1844
1897
|
use_when: "The user names one program and asks for a brief, targets, new targets, scope delta, or target breakdown.",
|
|
@@ -1895,14 +1948,10 @@ function bbradarGuide(topic, profile, client) {
|
|
|
1895
1948
|
then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
|
|
1896
1949
|
},
|
|
1897
1950
|
{
|
|
1898
|
-
user_goal: "
|
|
1899
|
-
first_tool: "
|
|
1900
|
-
arguments: { platforms: ["requested platforms"],
|
|
1901
|
-
|
|
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."
|
|
1951
|
+
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
1952
|
+
first_tool: "find_repository_hunt_targets",
|
|
1953
|
+
arguments: { platforms: ["requested platforms"], surface_mode: "web3", min_bounty_max: 5000, min_target_score: 60 },
|
|
1954
|
+
then: "Use this single composite call instead of manually chaining program discovery and repository tools. Omit or change any example filter the user did not request."
|
|
1906
1955
|
},
|
|
1907
1956
|
{
|
|
1908
1957
|
user_goal: "latest added wildcard targets with platform, reward, or report-count filters",
|
|
@@ -2019,16 +2068,6 @@ function coreBbradarGuide(topic, client) {
|
|
|
2019
2068
|
first_tool: "find_programs",
|
|
2020
2069
|
arguments: { max_results: 10, output_mode: "compact" }
|
|
2021
2070
|
},
|
|
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
2071
|
{
|
|
2033
2072
|
user_goal: "named program targets",
|
|
2034
2073
|
first_tool: "get_program_eligible_targets_by_name",
|
|
@@ -2050,14 +2089,10 @@ function coreBbradarGuide(topic, client) {
|
|
|
2050
2089
|
arguments: { include_targets: false, max_cursor_pages: 2 }
|
|
2051
2090
|
},
|
|
2052
2091
|
{
|
|
2053
|
-
user_goal: "
|
|
2054
|
-
first_tool: "
|
|
2055
|
-
arguments: { platforms: ["requested platforms"],
|
|
2056
|
-
|
|
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."
|
|
2092
|
+
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
2093
|
+
first_tool: "find_repository_hunt_targets",
|
|
2094
|
+
arguments: { platforms: ["requested platforms"], surface_mode: "web3", min_bounty_max: 5000, min_target_score: 60 },
|
|
2095
|
+
then: "Use this one call instead of manually chaining program and repository tools. Omit or change any example filter the user did not request."
|
|
2061
2096
|
},
|
|
2062
2097
|
{
|
|
2063
2098
|
user_goal: "export targets",
|
|
@@ -2084,6 +2119,7 @@ function coreBbradarGuide(topic, client) {
|
|
|
2084
2119
|
},
|
|
2085
2120
|
primary_entry_points: [
|
|
2086
2121
|
"find_programs",
|
|
2122
|
+
"find_repository_hunt_targets",
|
|
2087
2123
|
"run_program_name_action",
|
|
2088
2124
|
"get_program_eligible_targets_by_name",
|
|
2089
2125
|
"find_programs_by_target",
|
|
@@ -6258,6 +6294,285 @@ function clampLimit(name, value, max, warnings) {
|
|
|
6258
6294
|
warnings.push(`${name} was clamped from ${value} to ${max}.`);
|
|
6259
6295
|
return max;
|
|
6260
6296
|
}
|
|
6297
|
+
async function findRepositoryHuntTargets(client, config, input) {
|
|
6298
|
+
const warnings = [];
|
|
6299
|
+
const repositoryRequestReserve = input.upstream_request_budget >= 3 ? 2 : 1;
|
|
6300
|
+
const programRequestBudget = Math.max(1, input.upstream_request_budget - repositoryRequestReserve);
|
|
6301
|
+
const programInput = {
|
|
6302
|
+
platforms: input.platforms,
|
|
6303
|
+
exclude_platforms: input.exclude_platforms,
|
|
6304
|
+
tags: input.tags,
|
|
6305
|
+
scope_tags: input.scope_tags,
|
|
6306
|
+
target_types: input.target_types,
|
|
6307
|
+
language_tags: input.language_tags,
|
|
6308
|
+
web3: input.surface_mode === "web3",
|
|
6309
|
+
exclude_web3: input.surface_mode === "web2",
|
|
6310
|
+
opportunity_levels: input.opportunity_levels,
|
|
6311
|
+
updated_since: undefined,
|
|
6312
|
+
min_bounty_min: undefined,
|
|
6313
|
+
min_bounty_max: input.min_bounty_max,
|
|
6314
|
+
max_bounty_max: undefined,
|
|
6315
|
+
require_bounty: input.require_bounty,
|
|
6316
|
+
max_public_report_count: input.max_public_report_count,
|
|
6317
|
+
require_known_report_count: false,
|
|
6318
|
+
include_unknown_report_count: input.include_unknown_report_count,
|
|
6319
|
+
exclude_ctf: true,
|
|
6320
|
+
exclude_mobile_only: false,
|
|
6321
|
+
exclude_non_web: input.surface_mode === "web2",
|
|
6322
|
+
has_api_scope: false,
|
|
6323
|
+
contest_like: false,
|
|
6324
|
+
vdp_mode: "exclude",
|
|
6325
|
+
fresh_launch_days: undefined,
|
|
6326
|
+
min_wildcards: input.min_wildcards,
|
|
6327
|
+
min_eligible_targets: input.min_eligible_targets,
|
|
6328
|
+
min_total_targets: input.min_total_targets,
|
|
6329
|
+
min_added_24h: undefined,
|
|
6330
|
+
min_added_7d: input.min_added_7d,
|
|
6331
|
+
max_pages: input.max_program_pages,
|
|
6332
|
+
max_results: input.max_program_candidates,
|
|
6333
|
+
sort_by: "highest_reward",
|
|
6334
|
+
include_target_samples: false,
|
|
6335
|
+
target_sample_programs: 0,
|
|
6336
|
+
target_sample_size: 1,
|
|
6337
|
+
only_in_scope_targets: true,
|
|
6338
|
+
only_bounty_eligible_targets: true,
|
|
6339
|
+
target_samples_only_wildcards: false,
|
|
6340
|
+
output_mode: "compact",
|
|
6341
|
+
upstream_request_budget: programRequestBudget
|
|
6342
|
+
};
|
|
6343
|
+
const [programPayload, repository] = await Promise.all([
|
|
6344
|
+
findPrograms(client, config, programInput),
|
|
6345
|
+
fetchRepositoryHuntPage(client, input, warnings, repositoryRequestReserve)
|
|
6346
|
+
]);
|
|
6347
|
+
const candidatePrograms = readArray(programPayload.programs)
|
|
6348
|
+
.map(readObject)
|
|
6349
|
+
.filter((program) => program !== undefined);
|
|
6350
|
+
const candidatesById = new Map(candidatePrograms
|
|
6351
|
+
.map((program) => [stringField(program, "id"), program])
|
|
6352
|
+
.filter((entry) => entry[0] !== undefined));
|
|
6353
|
+
const changedSinceTime = input.changed_since ? Date.parse(input.changed_since) : undefined;
|
|
6354
|
+
const rankedMatches = [];
|
|
6355
|
+
let qualifyingTargetCount = 0;
|
|
6356
|
+
for (const rawChange of readArray(readObject(repository.api.data)?.results)) {
|
|
6357
|
+
const change = sanitizeRepositoryChange(rawChange, config.webBaseUrl);
|
|
6358
|
+
const observedAtText = stringField(change, "observed_at");
|
|
6359
|
+
const observedAt = observedAtText ? Date.parse(observedAtText) : 0;
|
|
6360
|
+
if (changedSinceTime !== undefined && (!Number.isFinite(observedAt) || observedAt < changedSinceTime)) {
|
|
6361
|
+
continue;
|
|
6362
|
+
}
|
|
6363
|
+
const programs = readArray(change.programs)
|
|
6364
|
+
.map((program) => stringField(readObject(program), "id"))
|
|
6365
|
+
.filter((id) => id !== undefined)
|
|
6366
|
+
.map((id) => candidatesById.get(id))
|
|
6367
|
+
.filter((program) => program !== undefined);
|
|
6368
|
+
if (programs.length === 0) {
|
|
6369
|
+
continue;
|
|
6370
|
+
}
|
|
6371
|
+
const targets = readArray(change.targets)
|
|
6372
|
+
.map(readObject)
|
|
6373
|
+
.filter((target) => target !== undefined)
|
|
6374
|
+
.filter((target) => repositoryHuntTargetMatches(target, input))
|
|
6375
|
+
.sort((left, right) => (readNumber(right.target_opportunity_score) ?? 0) - (readNumber(left.target_opportunity_score) ?? 0));
|
|
6376
|
+
if (targets.length === 0) {
|
|
6377
|
+
continue;
|
|
6378
|
+
}
|
|
6379
|
+
qualifyingTargetCount += targets.length;
|
|
6380
|
+
const maxReward = Math.max(0, ...programs.map(programMaximumReward));
|
|
6381
|
+
const maxTargetScore = Math.max(0, ...targets.map((target) => readNumber(target.target_opportunity_score) ?? 0));
|
|
6382
|
+
const balancedScore = repositoryHuntBalancedScore(maxReward, maxTargetScore, observedAt);
|
|
6383
|
+
const formattedChange = formatRepositoryChange(change, input.output_mode);
|
|
6384
|
+
const { programs: _eventPrograms, targets: _eventTargets, ...repositoryChange } = formattedChange;
|
|
6385
|
+
rankedMatches.push({
|
|
6386
|
+
balancedScore,
|
|
6387
|
+
maxReward,
|
|
6388
|
+
maxTargetScore,
|
|
6389
|
+
observedAt,
|
|
6390
|
+
payload: stripUndefined({
|
|
6391
|
+
repository_change: repositoryChange,
|
|
6392
|
+
programs,
|
|
6393
|
+
targets: targets.slice(0, input.max_targets_per_result).map((target) => input.output_mode === "full" ? target : compactTarget(target)),
|
|
6394
|
+
target_count: targets.length,
|
|
6395
|
+
max_target_score: maxTargetScore,
|
|
6396
|
+
max_program_reward: maxReward,
|
|
6397
|
+
match_score: balancedScore,
|
|
6398
|
+
why_ranked_here: repositoryHuntReasons(maxReward, maxTargetScore, observedAt)
|
|
6399
|
+
})
|
|
6400
|
+
});
|
|
6401
|
+
}
|
|
6402
|
+
rankedMatches.sort((left, right) => compareRepositoryHuntMatches(left, right, input.ranking_mode));
|
|
6403
|
+
const matches = rankedMatches.slice(0, input.max_results).map((match, index) => ({
|
|
6404
|
+
rank: index + 1,
|
|
6405
|
+
...match.payload
|
|
6406
|
+
}));
|
|
6407
|
+
const matchedPrograms = uniqueProgramsFromRepositoryMatches(matches);
|
|
6408
|
+
const programWarnings = readArray(programPayload.warnings).filter((warning) => typeof warning === "string");
|
|
6409
|
+
if (candidatePrograms.length === 0) {
|
|
6410
|
+
warnings.push("No programs matched the reward and program-level filters. Relax one program filter and retry.");
|
|
6411
|
+
}
|
|
6412
|
+
else if (matches.length === 0) {
|
|
6413
|
+
warnings.push("Programs matched, but no recent repository event also contained a target that passed the target-quality filters. Relax one target or freshness filter and retry.");
|
|
6414
|
+
}
|
|
6415
|
+
const programBudget = readObject(programPayload.upstream_budget);
|
|
6416
|
+
const usedRequests = (readNumber(programBudget?.used) ?? 0) + repository.attempts;
|
|
6417
|
+
return stripUndefined({
|
|
6418
|
+
request_id: randomUUID(),
|
|
6419
|
+
fetched_at: new Date().toISOString(),
|
|
6420
|
+
source_requests: [
|
|
6421
|
+
...readArray(programPayload.source_requests),
|
|
6422
|
+
{
|
|
6423
|
+
source: "repository_changes",
|
|
6424
|
+
request_id: repository.api.requestId,
|
|
6425
|
+
upstream_request_id: repository.api.upstreamRequestId,
|
|
6426
|
+
page: 1,
|
|
6427
|
+
page_size: repository.pageSize,
|
|
6428
|
+
...apiSourceMetadata(repository.api)
|
|
6429
|
+
}
|
|
6430
|
+
],
|
|
6431
|
+
warnings: uniqueStrings([...programWarnings, ...warnings]),
|
|
6432
|
+
filters: stripUndefined({
|
|
6433
|
+
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
6434
|
+
exclude_platforms: input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
|
|
6435
|
+
tags: input.tags.length > 0 ? input.tags : undefined,
|
|
6436
|
+
scope_tags: input.scope_tags.length > 0 ? input.scope_tags : undefined,
|
|
6437
|
+
target_types: input.target_types.length > 0 ? input.target_types : undefined,
|
|
6438
|
+
language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
|
|
6439
|
+
surface_mode: input.surface_mode,
|
|
6440
|
+
opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
|
|
6441
|
+
min_bounty_max: input.min_bounty_max,
|
|
6442
|
+
require_bounty: input.require_bounty,
|
|
6443
|
+
max_public_report_count: input.max_public_report_count,
|
|
6444
|
+
min_eligible_targets: input.min_eligible_targets,
|
|
6445
|
+
min_total_targets: input.min_total_targets,
|
|
6446
|
+
min_wildcards: input.min_wildcards,
|
|
6447
|
+
min_added_7d: input.min_added_7d,
|
|
6448
|
+
changed_since: input.changed_since,
|
|
6449
|
+
free_submission_only: input.free_submission_only,
|
|
6450
|
+
min_target_score: input.min_target_score,
|
|
6451
|
+
target_opportunity_labels: input.target_opportunity_labels.length > 0 ? input.target_opportunity_labels : undefined,
|
|
6452
|
+
competition_risks: input.competition_risks.length > 0 ? input.competition_risks : undefined,
|
|
6453
|
+
ranking_mode: input.ranking_mode,
|
|
6454
|
+
output_mode: input.output_mode
|
|
6455
|
+
}),
|
|
6456
|
+
upstream_budget: {
|
|
6457
|
+
initial: input.upstream_request_budget,
|
|
6458
|
+
used: usedRequests,
|
|
6459
|
+
remaining: Math.max(0, input.upstream_request_budget - usedRequests)
|
|
6460
|
+
},
|
|
6461
|
+
ranking_scope: readObject(programPayload.ranking_scope),
|
|
6462
|
+
programs: matchedPrograms,
|
|
6463
|
+
matches,
|
|
6464
|
+
meta: {
|
|
6465
|
+
returned: matches.length,
|
|
6466
|
+
matched_programs: matchedPrograms.length,
|
|
6467
|
+
program_candidates: candidatePrograms.length,
|
|
6468
|
+
repository_events_scanned: readArray(readObject(repository.api.data)?.results).length,
|
|
6469
|
+
qualifying_targets_before_result_limit: qualifyingTargetCount,
|
|
6470
|
+
repository_page_size_used: repository.pageSize,
|
|
6471
|
+
repository_request_attempts: repository.attempts,
|
|
6472
|
+
parallel_source_fetch: true
|
|
6473
|
+
}
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
async function fetchRepositoryHuntPage(client, input, warnings, maxAttempts) {
|
|
6477
|
+
let pageSize = input.repository_page_size;
|
|
6478
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
6479
|
+
try {
|
|
6480
|
+
const api = await client.getRepositoryChanges(toQuery({
|
|
6481
|
+
include_targets: true,
|
|
6482
|
+
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
6483
|
+
tags: input.tags.length > 0 ? input.tags : undefined,
|
|
6484
|
+
free_submission_only: input.free_submission_only,
|
|
6485
|
+
include_removed: false,
|
|
6486
|
+
include_ineligible: false,
|
|
6487
|
+
include_out_of_scope: false,
|
|
6488
|
+
page: 1,
|
|
6489
|
+
page_size: pageSize
|
|
6490
|
+
}));
|
|
6491
|
+
return { api, pageSize, attempts: attempt };
|
|
6492
|
+
}
|
|
6493
|
+
catch (error) {
|
|
6494
|
+
if (!(error instanceof BBRadarApiError) || error.status !== 413 || attempt >= maxAttempts || pageSize <= 10) {
|
|
6495
|
+
throw error;
|
|
6496
|
+
}
|
|
6497
|
+
const reducedPageSize = Math.max(10, Math.floor(pageSize / 2));
|
|
6498
|
+
warnings.push(`Repository page_size was reduced from ${pageSize} to ${reducedPageSize} after a 413 association-limit response.`);
|
|
6499
|
+
pageSize = reducedPageSize;
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
6502
|
+
throw new Error("No repository-change response was collected.");
|
|
6503
|
+
}
|
|
6504
|
+
function repositoryHuntTargetMatches(target, input) {
|
|
6505
|
+
if (readBoolean(target.in_scope) !== true || readBoolean(target.eligible_for_bounty) !== true) {
|
|
6506
|
+
return false;
|
|
6507
|
+
}
|
|
6508
|
+
const score = readNumber(target.target_opportunity_score);
|
|
6509
|
+
if (score === undefined || score < input.min_target_score) {
|
|
6510
|
+
return false;
|
|
6511
|
+
}
|
|
6512
|
+
if (input.target_opportunity_labels.length > 0 &&
|
|
6513
|
+
!input.target_opportunity_labels.includes(stringField(target, "target_opportunity_label"))) {
|
|
6514
|
+
return false;
|
|
6515
|
+
}
|
|
6516
|
+
if (input.competition_risks.length > 0 &&
|
|
6517
|
+
!input.competition_risks.includes(stringField(target, "competition_risk"))) {
|
|
6518
|
+
return false;
|
|
6519
|
+
}
|
|
6520
|
+
if (input.target_types.length > 0 && !input.target_types.some((targetType) => targetMatchesRequestedType(target, targetType))) {
|
|
6521
|
+
return false;
|
|
6522
|
+
}
|
|
6523
|
+
if (input.scope_tags.length > 0 && !targetMatchesAnyTags(target, input.scope_tags, "scope_tags")) {
|
|
6524
|
+
return false;
|
|
6525
|
+
}
|
|
6526
|
+
if (input.language_tags.length > 0 && !targetMatchesAnyTags(target, input.language_tags, "language_tags")) {
|
|
6527
|
+
return false;
|
|
6528
|
+
}
|
|
6529
|
+
return true;
|
|
6530
|
+
}
|
|
6531
|
+
function targetMatchesAnyTags(target, requested, field) {
|
|
6532
|
+
const actual = new Set(readStringArrayField(target, field).map(normalizeTag));
|
|
6533
|
+
return requested.map(normalizeTag).some((value) => actual.has(value));
|
|
6534
|
+
}
|
|
6535
|
+
function programMaximumReward(program) {
|
|
6536
|
+
return readNumber(readObject(program.reward_range)?.max) ?? 0;
|
|
6537
|
+
}
|
|
6538
|
+
function repositoryHuntBalancedScore(maxReward, maxTargetScore, observedAt) {
|
|
6539
|
+
const rewardScore = Math.min(100, Math.log10(Math.max(1, maxReward)) / 7 * 100);
|
|
6540
|
+
const ageDays = observedAt > 0 ? Math.max(0, Date.now() - observedAt) / (24 * 60 * 60 * 1000) : 30;
|
|
6541
|
+
const freshnessScore = Math.max(0, 100 - Math.min(30, ageDays) / 30 * 100);
|
|
6542
|
+
return Math.round((maxTargetScore * 0.45 + rewardScore * 0.35 + freshnessScore * 0.2) * 100) / 100;
|
|
6543
|
+
}
|
|
6544
|
+
function repositoryHuntReasons(maxReward, maxTargetScore, observedAt) {
|
|
6545
|
+
const reasons = [`target score up to ${maxTargetScore}`, `maximum reward ${maxReward}`];
|
|
6546
|
+
if (observedAt > 0) {
|
|
6547
|
+
reasons.push(`repository activity observed ${new Date(observedAt).toISOString()}`);
|
|
6548
|
+
}
|
|
6549
|
+
return reasons;
|
|
6550
|
+
}
|
|
6551
|
+
function compareRepositoryHuntMatches(left, right, mode) {
|
|
6552
|
+
if (mode === "highest_reward") {
|
|
6553
|
+
return right.maxReward - left.maxReward || right.maxTargetScore - left.maxTargetScore || right.observedAt - left.observedAt;
|
|
6554
|
+
}
|
|
6555
|
+
if (mode === "best_target") {
|
|
6556
|
+
return right.maxTargetScore - left.maxTargetScore || right.maxReward - left.maxReward || right.observedAt - left.observedAt;
|
|
6557
|
+
}
|
|
6558
|
+
if (mode === "newest") {
|
|
6559
|
+
return right.observedAt - left.observedAt || right.maxTargetScore - left.maxTargetScore || right.maxReward - left.maxReward;
|
|
6560
|
+
}
|
|
6561
|
+
return right.balancedScore - left.balancedScore || right.observedAt - left.observedAt;
|
|
6562
|
+
}
|
|
6563
|
+
function uniqueProgramsFromRepositoryMatches(matches) {
|
|
6564
|
+
const programs = new Map();
|
|
6565
|
+
for (const match of matches) {
|
|
6566
|
+
for (const value of readArray(match.programs)) {
|
|
6567
|
+
const program = readObject(value);
|
|
6568
|
+
const id = stringField(program, "id");
|
|
6569
|
+
if (program && id && !programs.has(id)) {
|
|
6570
|
+
programs.set(id, program);
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
return [...programs.values()];
|
|
6575
|
+
}
|
|
6261
6576
|
async function findPrograms(client, config, input) {
|
|
6262
6577
|
const normalized = normalizeFindProgramsInput(input);
|
|
6263
6578
|
const warnings = normalized.warnings;
|