---
phase: 16-osint-threat-intel-mobile-dns-api-marketplaces
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - pkg/recon/sources/postman.go
  - pkg/recon/sources/postman_test.go
  - pkg/recon/sources/swaggerhub.go
  - pkg/recon/sources/swaggerhub_test.go
  - pkg/recon/sources/rapidapi.go
  - pkg/recon/sources/rapidapi_test.go
autonomous: true
requirements: [RECON-API-01, RECON-API-02]

must_haves:
  truths:
    - "Postman source searches public collections/workspaces for hardcoded API keys"
    - "SwaggerHub source searches published API definitions for embedded keys in examples"
    - "RapidAPI source searches public API listings for exposed credentials"
  artifacts:
    - path: "pkg/recon/sources/postman.go"
      provides: "PostmanSource implementing recon.ReconSource"
      contains: "func (s *PostmanSource) Sweep"
    - path: "pkg/recon/sources/swaggerhub.go"
      provides: "SwaggerHubSource implementing recon.ReconSource"
      contains: "func (s *SwaggerHubSource) Sweep"
    - path: "pkg/recon/sources/rapidapi.go"
      provides: "RapidAPISource implementing recon.ReconSource"
      contains: "func (s *RapidAPISource) Sweep"
  key_links:
    - from: "pkg/recon/sources/postman.go"
      to: "pkg/recon/sources/queries.go"
      via: "BuildQueries call"
      pattern: "BuildQueries\\(s\\.Registry"
    - from: "pkg/recon/sources/swaggerhub.go"
      to: "pkg/recon/sources/queries.go"
      via: "BuildQueries call"
      pattern: "BuildQueries\\(s\\.Registry"
---

<objective>
Implement Postman, SwaggerHub, and RapidAPI ReconSource modules for API marketplace scanning.

Purpose: Detect API keys hardcoded in public Postman collections, SwaggerHub API definitions, and RapidAPI listings where developers accidentally include real credentials in request examples.
Output: Three source files + tests in pkg/recon/sources/
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@pkg/recon/source.go
@pkg/recon/sources/httpclient.go
@pkg/recon/sources/queries.go
@pkg/recon/sources/sentry.go
@pkg/recon/sources/sentry_test.go
</context>

<interfaces>
From pkg/recon/source.go:
```go
type ReconSource interface {
    Name() string
    RateLimit() rate.Limit
    Burst() int
    RespectsRobots() bool
    Enabled(cfg Config) bool
    Sweep(ctx context.Context, query string, out chan<- Finding) error
}
```

From pkg/recon/sources/httpclient.go:
```go
func NewClient() *Client
func (c *Client) Do(ctx context.Context, req *http.Request) (*http.Response, error)
```

From pkg/recon/sources/queries.go:
```go
func BuildQueries(reg *providers.Registry, source string) []string
```
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Postman and SwaggerHub sources</name>
  <files>pkg/recon/sources/postman.go, pkg/recon/sources/postman_test.go, pkg/recon/sources/swaggerhub.go, pkg/recon/sources/swaggerhub_test.go</files>
  <action>
Create PostmanSource in postman.go:

- Struct: PostmanSource with BaseURL, Registry (*providers.Registry), Limiters (*recon.LimiterRegistry), Client (*Client) fields. Credentialless — Postman public API search does not require authentication.
- Name() returns "postman". RateLimit() returns rate.Every(3*time.Second). Burst() returns 3. RespectsRobots() false. Enabled() always true.
- Sweep(): Default BaseURL to "https://www.postman.com/_api". Use BuildQueries(s.Registry, "postman"). For each query: GET `{base}/ws/proxy?request=%2Fsearch%2Fall%3Fquerytext%3D{url-encoded query}%26size%3D10%26type%3Dall` (Postman's internal search proxy). Parse JSON response containing search results with collection/workspace metadata. For each result, fetch the collection detail: GET `{base}/collection/{collection-id}` or use the direct URL from search. Stringify the collection JSON and check with ciLogKeyPattern. Emit Finding with SourceType "recon:postman", Source as `https://www.postman.com/collection/{id}`.
- Alternative simpler approach: Use Postman's public network search at `https://www.postman.com/_api/ws/proxy` with the search endpoint. The response contains snippets — check snippets directly with ciLogKeyPattern without fetching full collections (faster, fewer requests).

Create SwaggerHubSource in swaggerhub.go:

- Struct: SwaggerHubSource with BaseURL, Registry (*providers.Registry), Limiters (*recon.LimiterRegistry), Client (*Client) fields. Credentialless.
- Name() returns "swaggerhub". RateLimit() returns rate.Every(3*time.Second). Burst() returns 3. RespectsRobots() false. Enabled() always true.
- Sweep(): Default BaseURL to "https://app.swaggerhub.com/apiproxy/specs". Use BuildQueries(s.Registry, "swaggerhub"). For each query: GET `{base}?specType=ANY&visibility=PUBLIC&query={url-encoded query}&limit=10&page=1`. Parse JSON `{"apis":[{"name":"...","url":"...","description":"...","properties":[{"type":"Swagger","url":"..."}]}]}`. For each API result, fetch the spec URL to get the full OpenAPI/Swagger JSON. Check the spec content with ciLogKeyPattern (keys often appear in example values, server URLs, and security scheme defaults). Emit Finding with SourceType "recon:swaggerhub", Source as the SwaggerHub URL.

Tests: httptest mocks for both. Postman: mock search returns results with key-like content in snippets. SwaggerHub: mock returns API list, spec fetch returns OpenAPI JSON with embedded key pattern.
  </action>
  <verify>
    <automated>cd /home/salva/Documents/apikey && go test ./pkg/recon/sources/ -run "TestPostman|TestSwaggerHub" -count=1 -v</automated>
  </verify>
  <done>PostmanSource searches public collections, SwaggerHubSource searches published API specs, both emit findings on ciLogKeyPattern match in response content</done>
</task>

<task type="auto">
  <name>Task 2: RapidAPI source</name>
  <files>pkg/recon/sources/rapidapi.go, pkg/recon/sources/rapidapi_test.go</files>
  <action>
Create RapidAPISource in rapidapi.go:

- Struct: RapidAPISource with BaseURL, Registry (*providers.Registry), Limiters (*recon.LimiterRegistry), Client (*Client) fields. Credentialless.
- Name() returns "rapidapi". RateLimit() returns rate.Every(3*time.Second). Burst() returns 3. RespectsRobots() false. Enabled() always true.
- Sweep(): Default BaseURL to "https://rapidapi.com". Use BuildQueries(s.Registry, "rapidapi"). For each query: GET `{base}/search/{url-encoded query}?sortBy=ByRelevance&page=1` — RapidAPI's search page. Parse the HTML response body or use the internal JSON API if available. Check content with ciLogKeyPattern. Focus on API listings that include code snippets and example requests where developers may have pasted real API keys. Emit Finding with SourceType "recon:rapidapi", Source as the API listing URL.
- Simpler approach: Since RapidAPI's internal search API may not be stable, treat this as a scraping source. GET the search page, read up to 512KB of HTML, and scan with ciLogKeyPattern. This catches keys in code examples, API descriptions, and documentation snippets visible on the public page.

Tests: httptest mock. Test Name(), Enabled() (always true), Sweep with mock HTML containing key patterns, Sweep with clean HTML returning zero findings.
  </action>
  <verify>
    <automated>cd /home/salva/Documents/apikey && go test ./pkg/recon/sources/ -run "TestRapidAPI" -count=1 -v</automated>
  </verify>
  <done>RapidAPISource searches public API listings for key patterns, credentialless, tests pass with httptest mocks</done>
</task>

</tasks>

<verification>
All three API marketplace sources compile and pass tests:
```bash
cd /home/salva/Documents/apikey && go test ./pkg/recon/sources/ -run "TestPostman|TestSwaggerHub|TestRapidAPI" -count=1 -v
go vet ./pkg/recon/sources/
```
</verification>

<success_criteria>
- postman.go, swaggerhub.go, rapidapi.go each implement recon.ReconSource
- All three are credentialless (Enabled always true)
- All use BuildQueries + ciLogKeyPattern (consistent with other sources)
- Tests pass with httptest mocks
</success_criteria>

<output>
After completion, create `.planning/phases/16-osint_threat_intel_mobile_dns_api_marketplaces/16-03-SUMMARY.md`
</output>
