---
phase: 15-osint_forums_collaboration_log_aggregators
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - pkg/recon/sources/elasticsearch.go
  - pkg/recon/sources/elasticsearch_test.go
  - pkg/recon/sources/grafana.go
  - pkg/recon/sources/grafana_test.go
  - pkg/recon/sources/sentry.go
  - pkg/recon/sources/sentry_test.go
  - pkg/recon/sources/kibana.go
  - pkg/recon/sources/kibana_test.go
  - pkg/recon/sources/splunk.go
  - pkg/recon/sources/splunk_test.go
autonomous: true
requirements:
  - RECON-LOG-01
  - RECON-LOG-02
  - RECON-LOG-03
must_haves:
  truths:
    - "Elasticsearch source searches exposed ES instances for documents containing API keys"
    - "Grafana source searches exposed Grafana dashboards for API keys in queries and annotations"
    - "Sentry source searches exposed Sentry instances for API keys in error reports"
    - "Kibana source searches exposed Kibana instances for API keys in saved objects"
    - "Splunk source searches exposed Splunk instances for API keys in log data"
  artifacts:
    - path: "pkg/recon/sources/elasticsearch.go"
      provides: "ElasticsearchSource implementing ReconSource"
      contains: "func (s *ElasticsearchSource) Sweep"
    - path: "pkg/recon/sources/grafana.go"
      provides: "GrafanaSource implementing ReconSource"
      contains: "func (s *GrafanaSource) Sweep"
    - path: "pkg/recon/sources/sentry.go"
      provides: "SentrySource implementing ReconSource"
      contains: "func (s *SentrySource) Sweep"
    - path: "pkg/recon/sources/kibana.go"
      provides: "KibanaSource implementing ReconSource"
      contains: "func (s *KibanaSource) Sweep"
    - path: "pkg/recon/sources/splunk.go"
      provides: "SplunkSource implementing ReconSource"
      contains: "func (s *SplunkSource) Sweep"
  key_links:
    - from: "pkg/recon/sources/elasticsearch.go"
      to: "pkg/recon/sources/httpclient.go"
      via: "Client.Do for ES _search API"
      pattern: "client\\.Do"
    - from: "pkg/recon/sources/grafana.go"
      to: "pkg/recon/sources/httpclient.go"
      via: "Client.Do for Grafana API"
      pattern: "client\\.Do"
---

<objective>
Implement five log aggregator ReconSource implementations: Elasticsearch, Grafana, Sentry, Kibana, and Splunk.

Purpose: Enable scanning exposed logging/monitoring dashboards where API keys frequently appear in log entries, error reports, and dashboard configurations. RECON-LOG-01 covers Elasticsearch+Kibana together, RECON-LOG-02 covers Grafana, RECON-LOG-03 covers Sentry. Splunk is an additional log aggregator that fits naturally in this category.
Output: 5 source files + 5 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>
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: Elasticsearch, Kibana, Splunk sources</name>
  <files>
    pkg/recon/sources/elasticsearch.go
    pkg/recon/sources/elasticsearch_test.go
    pkg/recon/sources/kibana.go
    pkg/recon/sources/kibana_test.go
    pkg/recon/sources/splunk.go
    pkg/recon/sources/splunk_test.go
  </files>
  <action>
Create three ReconSource implementations following the TravisCISource pattern. These target exposed instances discovered via the query parameter (e.g. `keyhunter recon --sources=elasticsearch --query="target-es.example.com"`).

**ElasticsearchSource** (elasticsearch.go):
- Name: "elasticsearch"
- RateLimit: rate.Every(2*time.Second), Burst: 3
- RespectsRobots: false (API-based)
- Enabled: always true (credentialless — targets exposed instances without auth)
- Sweep: Exposed Elasticsearch instances allow unauthenticated queries. For each BuildQueries keyword, POST `{base}/_search` with JSON body `{"query":{"query_string":{"query":"{keyword}"}},"size":20}`. Parse JSON `hits.hits[]._source` (stringify the _source object). Run ciLogKeyPattern against stringified source. Emit Finding with SourceType "recon:elasticsearch", Source as `{base}/{index}/{id}`.
- BaseURL default: "http://localhost:9200" (always overridden by query target)
- If BaseURL is the default and query does not look like a URL, return nil early (no target to scan).
- Read up to 512KB per response (ES responses can be large).

**KibanaSource** (kibana.go):
- Name: "kibana"
- RateLimit: rate.Every(2*time.Second), Burst: 3
- RespectsRobots: false (API-based)
- Enabled: always true (credentialless)
- Sweep: Exposed Kibana instances have a saved objects API. GET `{base}/api/saved_objects/_find?type=visualization&type=dashboard&search={keyword}&per_page=20` with header `kbn-xsrf: true`. Parse JSON `saved_objects[].attributes` (stringify). Run ciLogKeyPattern. Also try GET `{base}/api/saved_objects/_find?type=index-pattern&per_page=10` to discover index patterns, then query ES via Kibana proxy: GET `{base}/api/console/proxy?path=/{index}/_search&method=GET` with keyword query. Emit Finding with SourceType "recon:kibana".
- BaseURL default: "http://localhost:5601" (always overridden)

**SplunkSource** (splunk.go):
- Name: "splunk"
- RateLimit: rate.Every(3*time.Second), Burst: 2
- RespectsRobots: false (API-based)
- Enabled: always true (credentialless — targets exposed Splunk Web)
- Sweep: Exposed Splunk instances may allow unauthenticated search via REST API. For each BuildQueries keyword, GET `{base}/services/search/jobs/export?search=search+{keyword}&output_mode=json&count=20`. Parse JSON results, run ciLogKeyPattern. Emit Finding with SourceType "recon:splunk".
- BaseURL default: "https://localhost:8089" (always overridden)
- If no target, return nil early.

Tests: httptest mock servers. ES test mocks POST /_search returning hits with API key in _source. Kibana test mocks /api/saved_objects/_find. Splunk test mocks /services/search/jobs/export.
  </action>
  <verify>
    <automated>cd /home/salva/Documents/apikey && go test ./pkg/recon/sources/ -run "TestElasticsearch|TestKibana|TestSplunk" -count=1 -v</automated>
  </verify>
  <done>Three log aggregator sources compile, pass interface checks, tests confirm Sweep emits findings from mock API responses</done>
</task>

<task type="auto">
  <name>Task 2: Grafana and Sentry sources</name>
  <files>
    pkg/recon/sources/grafana.go
    pkg/recon/sources/grafana_test.go
    pkg/recon/sources/sentry.go
    pkg/recon/sources/sentry_test.go
  </files>
  <action>
Create two more ReconSource implementations.

**GrafanaSource** (grafana.go):
- Name: "grafana"
- RateLimit: rate.Every(2*time.Second), Burst: 3
- RespectsRobots: false (API-based)
- Enabled: always true (credentialless — targets exposed Grafana instances)
- Sweep: Exposed Grafana instances allow unauthenticated dashboard browsing when anonymous access is enabled. For each BuildQueries keyword:
  1. GET `{base}/api/search?query={keyword}&type=dash-db&limit=10` to find dashboards.
  2. For each dashboard, GET `{base}/api/dashboards/uid/{uid}` to get dashboard JSON.
  3. Stringify the dashboard JSON panels and targets, run ciLogKeyPattern.
  4. Also check `{base}/api/datasources` for data source configs that may contain credentials.
  Emit Finding with SourceType "recon:grafana", Source as dashboard URL.
- BaseURL default: "http://localhost:3000" (always overridden)

**SentrySource** (sentry.go):
- Name: "sentry"
- RateLimit: rate.Every(2*time.Second), Burst: 3
- RespectsRobots: false (API-based)
- Enabled: always true (credentialless — targets exposed Sentry instances)
- Sweep: Exposed Sentry instances (self-hosted) may have the API accessible. For each BuildQueries keyword:
  1. GET `{base}/api/0/issues/?query={keyword}&limit=10` to search issues.
  2. For each issue, GET `{base}/api/0/issues/{id}/events/?limit=5` to get events.
  3. Stringify event data (tags, breadcrumbs, exception values), run ciLogKeyPattern.
  Emit Finding with SourceType "recon:sentry".
- BaseURL default: "https://sentry.example.com" (always overridden)
- Error reports commonly contain API keys in request headers, environment variables, and stack traces.

Tests: httptest mock servers. Grafana test mocks /api/search + /api/dashboards/uid/{uid} returning dashboard JSON with API key. Sentry test mocks /api/0/issues/ + /api/0/issues/{id}/events/ returning event data with API key.
  </action>
  <verify>
    <automated>cd /home/salva/Documents/apikey && go test ./pkg/recon/sources/ -run "TestGrafana|TestSentry" -count=1 -v</automated>
  </verify>
  <done>Grafana and Sentry sources compile, pass interface checks, tests confirm Sweep emits findings from mock API 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 "TestElasticsearch|TestKibana|TestSplunk|TestGrafana|TestSentry" -count=1
</verification>

<success_criteria>
- All 5 log aggregator sources implement recon.ReconSource interface
- All 5 test files pass with httptest-based mocks
- Each source follows the established pattern (BuildQueries + Client.Do + ciLogKeyPattern)
- go vet and go build pass cleanly
</success_criteria>

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