---
phase: 15-osint_forums_collaboration_log_aggregators
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - pkg/recon/sources/stackoverflow.go
  - pkg/recon/sources/stackoverflow_test.go
  - pkg/recon/sources/reddit.go
  - pkg/recon/sources/reddit_test.go
  - pkg/recon/sources/hackernews.go
  - pkg/recon/sources/hackernews_test.go
  - pkg/recon/sources/discord.go
  - pkg/recon/sources/discord_test.go
  - pkg/recon/sources/slack.go
  - pkg/recon/sources/slack_test.go
  - pkg/recon/sources/devto.go
  - pkg/recon/sources/devto_test.go
autonomous: true
requirements:
  - RECON-FORUM-01
  - RECON-FORUM-02
  - RECON-FORUM-03
  - RECON-FORUM-04
  - RECON-FORUM-05
  - RECON-FORUM-06
must_haves:
  truths:
    - "StackOverflow source searches SE API for LLM keyword matches and scans content"
    - "Reddit source searches Reddit for LLM keyword matches and scans content"
    - "HackerNews source searches Algolia HN API for keyword matches and scans content"
    - "Discord source searches indexed Discord content for keyword matches"
    - "Slack source searches indexed Slack content for keyword matches"
    - "DevTo source searches dev.to API for keyword matches and scans articles"
  artifacts:
    - path: "pkg/recon/sources/stackoverflow.go"
      provides: "StackOverflowSource implementing ReconSource"
      contains: "func (s *StackOverflowSource) Sweep"
    - path: "pkg/recon/sources/reddit.go"
      provides: "RedditSource implementing ReconSource"
      contains: "func (s *RedditSource) Sweep"
    - path: "pkg/recon/sources/hackernews.go"
      provides: "HackerNewsSource implementing ReconSource"
      contains: "func (s *HackerNewsSource) Sweep"
    - path: "pkg/recon/sources/discord.go"
      provides: "DiscordSource implementing ReconSource"
      contains: "func (s *DiscordSource) Sweep"
    - path: "pkg/recon/sources/slack.go"
      provides: "SlackSource implementing ReconSource"
      contains: "func (s *SlackSource) Sweep"
    - path: "pkg/recon/sources/devto.go"
      provides: "DevToSource implementing ReconSource"
      contains: "func (s *DevToSource) Sweep"
  key_links:
    - from: "pkg/recon/sources/stackoverflow.go"
      to: "pkg/recon/sources/httpclient.go"
      via: "Client.Do for HTTP requests"
      pattern: "client\\.Do"
    - from: "pkg/recon/sources/hackernews.go"
      to: "pkg/recon/sources/httpclient.go"
      via: "Client.Do for Algolia API"
      pattern: "client\\.Do"
---

<objective>
Implement six forum/discussion ReconSource implementations: StackOverflow, Reddit, HackerNews, Discord, Slack, and DevTo.

Purpose: Enable scanning developer forums and discussion platforms where API keys are commonly shared in code examples, questions, and discussions.
Output: 6 source files + 6 test files 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
@.planning/STATE.md
@pkg/recon/source.go
@pkg/recon/sources/httpclient.go
@pkg/recon/sources/travisci.go
@pkg/recon/sources/travisci_test.go

<interfaces>
<!-- Executor must implement recon.ReconSource for each source -->
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/register.go:
```go
func BuildQueries(reg *providers.Registry, sourceName string) []string
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: StackOverflow, Reddit, HackerNews sources</name>
  <files>
    pkg/recon/sources/stackoverflow.go
    pkg/recon/sources/stackoverflow_test.go
    pkg/recon/sources/reddit.go
    pkg/recon/sources/reddit_test.go
    pkg/recon/sources/hackernews.go
    pkg/recon/sources/hackernews_test.go
  </files>
  <action>
Create three ReconSource implementations following the exact TravisCISource pattern (struct with BaseURL, Registry, Limiters, Client fields; interface compliance var check; BuildQueries for keywords).

**StackOverflowSource** (stackoverflow.go):
- Name: "stackoverflow"
- RateLimit: rate.Every(2*time.Second), Burst: 3
- RespectsRobots: false (API-based)
- Enabled: always true (credentialless, uses public API)
- Sweep: For each BuildQueries keyword, GET `{base}/2.3/search/excerpts?order=desc&sort=relevance&q={keyword}&site=stackoverflow` (Stack Exchange API v2.3). Parse JSON response with `items[].body` or `items[].excerpt`. Run ciLogKeyPattern regex against each item body. Emit Finding with SourceType "recon:stackoverflow", Source set to the question/answer URL.
- BaseURL default: "https://api.stackexchange.com"
- Limit response reading to 256KB per response.

**RedditSource** (reddit.go):
- Name: "reddit"
- RateLimit: rate.Every(2*time.Second), Burst: 2
- RespectsRobots: false (API/JSON endpoint)
- Enabled: always true (credentialless, uses public JSON endpoints)
- Sweep: For each BuildQueries keyword, GET `{base}/search.json?q={keyword}&sort=new&limit=25&restrict_sr=false` (Reddit JSON API, no OAuth needed for public search). Parse JSON `data.children[].data.selftext`. Run ciLogKeyPattern regex. Emit Finding with SourceType "recon:reddit".
- BaseURL default: "https://www.reddit.com"
- Set User-Agent to a descriptive string (Reddit blocks default UA).

**HackerNewsSource** (hackernews.go):
- Name: "hackernews"
- RateLimit: rate.Every(1*time.Second), Burst: 5
- RespectsRobots: false (Algolia API)
- Enabled: always true (credentialless)
- Sweep: For each BuildQueries keyword, GET `{base}/api/v1/search?query={keyword}&tags=comment&hitsPerPage=20` (Algolia HN Search API). Parse JSON `hits[].comment_text`. Run ciLogKeyPattern regex. Emit Finding with SourceType "recon:hackernews".
- BaseURL default: "https://hn.algolia.com"

Each test file follows travisci_test.go pattern: TestXxx_Name, TestXxx_Enabled, TestXxx_Sweep with httptest server returning mock JSON containing an API key pattern, asserting at least one finding with correct SourceType.
  </action>
  <verify>
    <automated>cd /home/salva/Documents/apikey && go test ./pkg/recon/sources/ -run "TestStackOverflow|TestReddit|TestHackerNews" -count=1 -v</automated>
  </verify>
  <done>Three forum sources compile, pass interface checks, and tests confirm Sweep emits findings from mock API responses</done>
</task>

<task type="auto">
  <name>Task 2: Discord, Slack, DevTo sources</name>
  <files>
    pkg/recon/sources/discord.go
    pkg/recon/sources/discord_test.go
    pkg/recon/sources/slack.go
    pkg/recon/sources/slack_test.go
    pkg/recon/sources/devto.go
    pkg/recon/sources/devto_test.go
  </files>
  <action>
Create three more ReconSource implementations following the same pattern.

**DiscordSource** (discord.go):
- Name: "discord"
- RateLimit: rate.Every(3*time.Second), Burst: 2
- RespectsRobots: false
- Enabled: always true (credentialless, uses search engine dorking approach)
- Sweep: Discord does not have a public content search API. Use Google-style dorking approach: for each BuildQueries keyword, GET `{base}/search?q=site:discord.com+{keyword}&format=json` against a configurable search endpoint. In practice this source discovers Discord content indexed by search engines. Parse response for URLs and content, run ciLogKeyPattern. Emit Finding with SourceType "recon:discord".
- BaseURL default: "https://search.discobot.dev" (placeholder, overridden in tests via BaseURL)
- This is a best-effort scraping source since Discord has no public API for message search.

**SlackSource** (slack.go):
- Name: "slack"
- RateLimit: rate.Every(3*time.Second), Burst: 2
- RespectsRobots: false
- Enabled: always true (credentialless, uses search engine dorking approach)
- Sweep: Similar to Discord - Slack messages are not publicly searchable via API without workspace auth. Use dorking approach: for each keyword, GET `{base}/search?q=site:slack-archive.org+OR+site:slack-files.com+{keyword}&format=json`. Parse results, run ciLogKeyPattern. Emit Finding with SourceType "recon:slack".
- BaseURL default: "https://search.slackarchive.dev" (placeholder, overridden in tests)

**DevToSource** (devto.go):
- Name: "devto"
- RateLimit: rate.Every(1*time.Second), Burst: 5
- RespectsRobots: false (API-based)
- Enabled: always true (credentialless, public API)
- Sweep: For each BuildQueries keyword, GET `{base}/api/articles?tag={keyword}&per_page=10&state=rising` (dev.to public API). Parse JSON array of articles, for each article fetch `{base}/api/articles/{id}` to get `body_markdown`. Run ciLogKeyPattern. Emit Finding with SourceType "recon:devto".
- BaseURL default: "https://dev.to"
- Limit to first 5 articles to stay within rate limits.

Each test file: TestXxx_Name, TestXxx_Enabled, TestXxx_Sweep with httptest mock server. Discord and Slack tests mock the search endpoint returning results with API key content. DevTo test mocks /api/articles list and /api/articles/{id} detail endpoint.
  </action>
  <verify>
    <automated>cd /home/salva/Documents/apikey && go test ./pkg/recon/sources/ -run "TestDiscord|TestSlack|TestDevTo" -count=1 -v</automated>
  </verify>
  <done>Three more forum/messaging sources compile, pass interface checks, and tests confirm Sweep emits findings from mock responses</done>
</task>

</tasks>

<verification>
cd /home/salva/Documents/apikey && go build ./... && go vet ./pkg/recon/sources/
cd /home/salva/Documents/apikey && go test ./pkg/recon/sources/ -run "TestStackOverflow|TestReddit|TestHackerNews|TestDiscord|TestSlack|TestDevTo" -count=1
</verification>

<success_criteria>
- All 6 forum sources implement recon.ReconSource interface
- All 6 test files pass with httptest-based mocks
- Each source uses BuildQueries + Client.Do + ciLogKeyPattern (or similar) pattern
- go vet and go build pass cleanly
</success_criteria>

<output>
After completion, create `.planning/phases/15-osint_forums_collaboration_log_aggregators/15-01-SUMMARY.md`
</output>
