Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ workflow built for triage—not another folder full of uncorrelated text files.
testing for authorized network engagements.
- **Designed for long scans:** resumable tasks, persistent data, monitoring,
live logs, reports and resource-aware scheduling.
- **Program-aware projects:** browse public HackerOne, Bugcrowd, Intigriti and
YesWeHack programs, filter their declared scope, import only selected assets
and review scope changes before Reconner expands a scan.
- **Works on phones:** the operations dashboard, navigation, scan activity and
update center adapt to mobile screens and touch input.

Expand Down Expand Up @@ -159,6 +162,39 @@ Requests also share a bounded per-host gate so independently scheduled modules
do not multiply into accidental WAF pressure. Different targets can still make
progress in parallel within the configured CPU and memory budget.

## Bug bounty catalog and Projects

The **Bounty programs** menu normalizes public HackerOne, Bugcrowd, Intigriti
and YesWeHack programs into one local catalog. It supports search and server-side pagination plus
filters for provider, live status, bounty/VDP, declared in-scope asset count,
wildcards, asset type, reward, industry, Safe Harbor and start/update order.
The cache refreshes every six hours; an administrator can also request a
background refresh from the dashboard. Provider indexes are cached first;
structured scope is fetched lazily when a program is opened/imported, while
programs linked to monitored Projects refresh every six hours. This keeps memory,
bandwidth and provider load bounded. A provider outage leaves the last good
catalog available and retries with backoff.

Opening a program loads its current structured scope. Select the assets you want
and create a Project, or create a Project manually and mix domains, wildcards,
exact URLs/pages, JavaScript files, APIs, IPs and CIDRs. Exact page and JS assets
are seeded directly into their relevant analysis pipeline instead of being
reduced to a hostname.

Program scope remains controlled by the operator:

- newly published upstream assets become pending scope events and are never
scanned before explicit approval;
- removed, private or submission-ineligible assets are suspended immediately,
while their findings and history remain intact;
- modified scope instructions or eligibility generate a review event and a
dashboard notification;
- monitoring records normalized page/HTTP/security/JavaScript diffs, then
schedules only the relevant verification modules when something changes.

The catalog is a convenience cache, not legal authorization. Always verify the
official program brief, exclusions and rules of engagement before scanning.

## Web reconnaissance and DAST

Reconner includes:
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.0
1.3.0
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "reconner-frontend",
"version": "1.1.0",
"version": "1.3.0",
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import TargetDetail from './pages/TargetDetail'
import Findings from './pages/Findings'
import Tasks from './pages/Tasks'
import System from './pages/System'
import BountyPrograms from './pages/BountyPrograms'
import { Spinner } from './components/ui'

function AuthGuard({ children }: { children: React.ReactNode }) {
Expand Down Expand Up @@ -45,6 +46,7 @@ export default function App() {
<Route index element={<Dashboard/>}/>
<Route path="targets" element={<Targets/>}/>
<Route path="targets/:id" element={<TargetDetail/>}/>
<Route path="bounty-programs" element={<BountyPrograms/>}/>
<Route path="findings" element={<Findings/>}/>
<Route path="tasks" element={<Tasks/>}/>
<Route path="system" element={<System/>}/>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const Icons = {
),
web: I('M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM3 12h18M12 3c2.5 2.5 3.8 5.7 3.8 9S14.5 18.5 12 21c-2.5-2.5-3.8-5.7-3.8-9S9.5 5.5 12 3z'),
network: I('M6 4h12M6 4v4M18 4v4M4 8h16v4H4V8zM8 16h8M8 16v4M16 16v4M6 12v4M18 12v4'),
bounty: I('M8 4h8v3a4 4 0 0 1-8 0V4zM6 5H4v2a4 4 0 0 0 4 4M18 5h2v2a4 4 0 0 1-4 4M12 11v5M8 20h8M9 16h6v4H9z'),
findings: I('M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z'),
tasks: I('M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01'),
system: I('M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'),
Expand Down Expand Up @@ -64,7 +65,8 @@ export const Sidebar = () => {
const groups: NavGroup[] = [
{ label: 'Command center', items: [{ to: '/', label: 'Overview', Icon: Icons.dashboard }] },
{ label: 'Operations', items: [
{ to: '/targets', label: 'Targets', Icon: Icons.targets, badge: targetsN > 0 ? { text: String(targetsN) } : null },
{ to: '/bounty-programs', label: 'Bounty programs', Icon: Icons.bounty },
{ to: '/targets', label: 'Projects', Icon: Icons.targets, badge: targetsN > 0 ? { text: String(targetsN) } : null },
{ to: '/findings', label: 'Findings', Icon: Icons.findings },
{ to: '/tasks', label: 'Scan activity', Icon: Icons.tasks, badge: runningN > 0 ? { text: String(runningN), live: true } : null },
] },
Expand Down
29 changes: 26 additions & 3 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Parameter, DirectoryFinding, BackupFinding, OpenRedirectFinding,
NucleiFinding, VulnFinding, MonitoringChange, Task, TaskLog, DashboardStats, AttackPath,
NetworkService, IngramCamera, Asset,
BountyProgram, BountySyncState, BountyScopeEvent,
} from '../types'

const BASE = '/api'
Expand Down Expand Up @@ -82,9 +83,9 @@ export const targets = {
req<Target>('/targets', { method: 'POST', body: JSON.stringify(data) }),
networkServices: (id: string) => req<NetworkService[]>(`/targets/${id}/network-services`),
assets: (id: string) => req<Asset[]>(`/targets/${id}/assets`),
addAsset: (id: string, value: string, name: string) =>
req<Asset>(`/targets/${id}/assets`, { method: 'POST', body: JSON.stringify({ value, name }) }),
updateAsset: (id: string, aid: string, data: { name?: string; value?: string }) =>
addAsset: (id: string, value: string, name: string, assetType?: string) =>
req<Asset>(`/targets/${id}/assets`, { method: 'POST', body: JSON.stringify({ value, name, asset_type: assetType }) }),
updateAsset: (id: string, aid: string, data: { name?: string; value?: string; asset_type?: string }) =>
req<void>(`/targets/${id}/assets/${aid}`, { method: 'PATCH', body: JSON.stringify(data) }),
deleteAsset: (id: string, aid: string) =>
req<void>(`/targets/${id}/assets/${aid}`, { method: 'DELETE' }),
Expand Down Expand Up @@ -134,6 +135,28 @@ export const targets = {
},
}

export interface BountyProgramList {
programs: BountyProgram[]
total: number
page: number
limit: number
}

export const bounty = {
list: (params?: Record<string, string>) => {
const q = params ? '?' + new URLSearchParams(params).toString() : ''
return req<BountyProgramList>(`/bounty/programs${q}`)
},
get: (id: string) => req<BountyProgram>(`/bounty/programs/${id}`),
status: () => req<BountySyncState[]>('/bounty/status'),
sync: () => req<{ status: string }>('/bounty/sync', { method: 'POST' }),
createProject: (id: string, body: { name?: string; description?: string; priority?: string; notes?: string; asset_ids: string[]; monitor_enabled: boolean; monitor_interval_hours: number }) =>
req<{ id: string; url: string }>(`/bounty/programs/${id}/projects`, { method: 'POST', body: JSON.stringify(body) }),
events: (targetId: string) => req<BountyScopeEvent[]>(`/targets/${targetId}/bounty-events`),
resolveEvent: (targetId: string, eventId: string, decision: 'approve' | 'reject') =>
req<{ status: string }>(`/targets/${targetId}/bounty-events/${eventId}`, { method: 'POST', body: JSON.stringify({ decision }) }),
}

export const findings = {
subdomains: (id: string, p?: Record<string,string>) => req<Subdomain[]>(`/targets/${id}/subdomains${p ? '?'+new URLSearchParams(p) : ''}`),
httpServices: (id: string) => req<HTTPService[]>(`/targets/${id}/http-services`),
Expand Down
Loading
Loading