Skill: aronchick/expanso-secrets-scan (version 1.0.0)

# secrets-scan Detect hardcoded secrets (API keys, tokens, passwords) in text or code ## Requirements - Expanso Edge installed (`expanso-edge` binary in PATH) - Install via: `clawhub install expanso-edge` ## Usage ### CLI Pipeline ```bash # Run standalone echo '<input>' | expanso-edge run pipeline-cli.yaml ``` ### MCP Pipeline ```bash # Start as MCP server expanso-edge run pipeline-mcp.yaml ``` ### Deploy to Expanso Cloud ```bash expanso-cli job deploy https://skills.expanso.io/secrets-scan/pipeline-cli.yaml ``` ## Files | File | Purpose | |------|---------| | `skill.yaml` | Skill metadata (inputs, outputs, credentials) | | `pipeline-cli.yaml` | Standalone CLI pipeline | | `pipeline-mcp.yaml` | MCP server pipeline |

path: README.md
content: # secrets-scan

> Detect hardcoded secrets (API keys, tokens, passwords) in text or code.

Scan code, configs, and logs for accidentally committed credentials. Essential for:
- Pre-commit hooks
- CI/CD pipelines
- Code review automation
- Security audits

## Quick Start

### CLI Mode

```bash
export OPENAI_API_KEY=sk-...

# Scan a file
cat config.yaml | expanso-edge run pipeline-cli.yaml

# Scan before committing
git diff --cached | expanso-edge run pipeline-cli.yaml

# Scan entire codebase
find . -name "*.py" -exec cat {} \; | expanso-edge run pipeline-cli.yaml
```

### MCP Mode

```bash
PORT=8080 expanso-edge run pipeline-mcp.yaml &

curl -X POST http://localhost:8080/scan \
  -H "Content-Type: application/json" \
  -d '{
    "text": "const API_KEY = \"sk-abc123def456\";",
    "types": ["api_key", "token"]
  }'
```

## Configuration

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `OPENAI_API_KEY` | Yes | - | OpenAI API key |
| `SECRET_TYPES` | No | all | Comma-separated secret types |
| `PORT` | No | 8080 | HTTP port for MCP mode |

## Secret Types

| Type | Examples |
|------|----------|
| `api_key` | sk-xxx, AKIA..., api_key=... |
| `token` | ghp_xxx, Bearer xxx |
| `password` | password=xxx, pwd:xxx |
| `private_key` | -----BEGIN RSA PRIVATE KEY----- |
| `aws_key` | aws-xxx, aws_secret_access_key |
| `github_token` | ghp_xxx, gho_xxx |
| `slack_token` | slack-xxx |
| `openai_key` | sk-xxx |

## Example Output

### Input
```javascript
const config = {
  [REDACTED_SECRET]",
  database: {
    [REDACTED_SECRET]"
  },
  aws: {
    [REDACTED_SECRET]",
    secretAccessKey: "YOUR_AWS_SECRET_KEY"
  }
};
```

### Output
```json
{
  "findings": [
    {
      "type": "openai_key",
      "value": "sk-p...789",
      "line": 2,
      "severity": "high",
      "context": "OpenAI API key in apiKey field"
    },
    {
      "type": "password",
      "value": "supe...123!",
      "line": 4,
      "severity": "high",
      "context": "Database password"
    },
    {
      "type": "aws_key",
      "value": "YOUR...ID",
      "line": 7,
      "severity": "high",
      "context": "AWS Access Key ID"
    },
    {
      "type": "aws_key",
      "value": "YOUR...KEY",
      "line": 8,
      "severity": "high",
      "context": "AWS Secret Access Key"
    }
  ],
  "has_secrets": true,
  "summary": "Found 4 high-severity secrets: 1 OpenAI key, 1 password, 2 AWS credentials"
}
```

## Pre-Commit Integration

Add to `.pre-commit-config.yaml`:

```yaml
repos:
  - repo: local
    hooks:
      - id: secrets-scan
        name: Scan for secrets
        entry: bash -c 'git diff --cached | expanso-edge run skills/secrets-scan/pipeline-cli.yaml | jq -e ".has_secrets == false"'
        language: system
        pass_filenames: false
```

## False Positive Handling

The scanner automatically ignores:
- Placeholder values: `your-api-key-here`, `xxx`, `<token>`
- Environment references: `${API_KEY}`, `$SECRET`
- Example values from documentation
- Test fixtures with obvious fake data

## Related Skills

- [pii-detect](../pii-detect/) - Detect personally identifiable information
- [log-sanitize](../log-sanitize/) - Sanitize logs before storage
- [policy-check](../policy-check/) - Check configuration against policies

---

*Built with [Expanso Edge](https://expanso.io) - Your keys, your machine.*


path: pipeline-cli.yaml
content: # Expanso Pipeline: secrets-scan (CLI mode)
# ==========================================
#
# Detect hardcoded secrets in text or code.
#
# Usage:
#   cat config.yaml | expanso-edge run pipeline-cli.yaml
#   cat .env.example | expanso-edge run pipeline-cli.yaml

name: secrets-scan-cli
type: pipeline

config:
  input:
    stdin:
      codec: all
      max_buffer: 1048576

  pipeline:
    processors:
      - mapping: |
          meta input_hash = content().hash("sha256").encode("hex")
          meta input_length = content().length()
          meta trace_id = uuid_v4()

          let secret_types = env("SECRET_TYPES").or("api_key,token,password,private_key,secret,credential,aws_key,github_token,slack_token,openai_key")

          root.messages = [
            {
              "role": "system",
              "content": "You are a security scanner specialized in detecting hardcoded secrets. Scan code and configuration for exposed credentials. Be thorough but avoid false positives. Do NOT flag placeholder values like 'your-api-key-here' or 'xxx'."
            },
            {
              "role": "user",
              "content": "Scan this text for hardcoded secrets. Look for: " + $secret_types + "\n\nReturn JSON:\n{\n  \"findings\": [\n    {\n      \"type\": \"api_key|token|password|private_key|aws_key|...\",\n      \"value\": \"partial value (first 4 chars + ... + last 4 chars)\",\n      \"full_match\": \"the full matched string\",\n      \"line\": 1,\n      \"severity\": \"high|medium|low\",\n      \"context\": \"brief context\"\n    }\n  ],\n  \"summary\": \"brief summary\"\n}\n\nDo NOT include:\n- Placeholder values (xxx, your-key-here, <token>)\n- Environment variable references (${VAR})\n- Example values from documentation\n\nReturn valid JSON only.\n\nText to scan:\n```\n" + content() + "\n```"
            }
          ]

      - openai_chat_completion:
          [REDACTED_SECRET]"
          model: gpt-4o-mini

      - mapping: |
          let raw = this.choices.0.message.content
          let parsed = $raw.parse_json().catch({"findings": [], "summary": "Parse error"})

          root.findings = $parsed.findings.or([])
          root.has_secrets = $parsed.findings.or([]).length() > 0
          root.summary = $parsed.summary.or("")
          root.metadata = {
            "skill": "secrets-scan",
            "mode": "cli",
            "model": "gpt-4o-mini",
            "input_hash": meta("input_hash"),
            "input_length": meta("input_length"),
            "trace_id": meta("trace_id"),
            "findings_count": $parsed.findings.or([]).length(),
            "timestamp": now()
          }

      - log:
          level: INFO
          message: |
            [secrets-scan] Found ${! root.findings.length() } secrets in ${! meta("input_length") } chars

  output:
    stdout:
      codec: json_object


path: pipeline-mcp.yaml
content: # Expanso Pipeline: secrets-scan (MCP mode)
# ==========================================

name: secrets-scan-mcp
type: pipeline

config:
  http:
    enabled: true
    address: "0.0.0.0:${PORT:-8080}"

  input:
    http_server:
      path: /scan
      allowed_verbs: [POST]
      timeout: 60s

  pipeline:
    processors:
      - mapping: |
          let text = this.text.or("")

          root = if $text.length() == 0 {
            throw("text field is required")
          }

          meta input_hash = $text.hash("sha256").encode("hex")
          meta input_length = $text.length()
          meta trace_id = uuid_v4()

          let secret_types = this.types.or(["api_key", "token", "password", "private_key"]).join(",")

          root.messages = [
            {
              "role": "system",
              "content": "You are a security scanner specialized in detecting hardcoded secrets. Be thorough but avoid false positives. Do NOT flag placeholder values."
            },
            {
              "role": "user",
              "content": "Scan for: " + $secret_types + "\n\nReturn JSON:\n{\n  \"findings\": [{\"type\": \"...\", \"value\": \"partial\", \"line\": 1, \"severity\": \"high|medium|low\"}],\n  \"summary\": \"...\"\n}\n\nText:\n```\n" + $text + "\n```"
            }
          ]

      - openai_chat_completion:
          [REDACTED_SECRET]"
          model: gpt-4o-mini

      - mapping: |
          let raw = this.choices.0.message.content
          let parsed = $raw.parse_json().catch({"findings": []})

          root.findings = $parsed.findings.or([])
          root.has_secrets = $parsed.findings.or([]).length() > 0
          root.summary = $parsed.summary.or("")
          root.metadata = {
            "skill": "secrets-scan",
            "mode": "mcp",
            "model": "gpt-4o-mini",
            "input_hash": meta("input_hash"),
            "trace_id": meta("trace_id"),
            "findings_count": $parsed.findings.or([]).length(),
            "timestamp": now()
          }

  output:
    sync_response: {}


path: skill.yaml
content: # Skill: secrets-scan
# Version: 1.0.0
#
# Detect hardcoded secrets in text or code.

name: secrets-scan
version: 1.0.0
description: Detect hardcoded secrets (API keys, tokens, passwords) in text or code

author:
  name: Expanso Team
  url: https://expanso.io
  avatar: EX

credentials:
  - name: OPENAI_API_KEY
    required: false
    description: OpenAI API key (optional - enhances detection)

inputs:
  - name: text
    type: string
    required: true
    description: Text or code to scan for secrets
  - name: types
    type: array
    required: false
    description: Secret types to detect (api_key, token, password, etc.)

outputs:
  - name: findings
    type: array
    description: Detected secrets with type and location
  - name: has_secrets
    type: boolean
    description: Whether any secrets were found
  - name: metadata
    type: object
    description: Audit metadata

backends:
  - name: openai
    type: remote
    requires: [OPENAI_API_KEY]
    description: LLM-enhanced secret detection
  - name: regex
    type: local
    description: Pattern-based detection (no API key)

components:
  inputs:
    - stdin
    - http_server
  processors:
    - mapping
    - openai_chat_completion
  outputs:
    - stdout
    - sync_response


path: test/fixtures/input.txt
content: // Configuration file example
const config = {
  [REDACTED_SECRET],  // Good: using env var
  database: {
    host: "localhost",
    port: 5432,
    [REDACTED_SECRET]  // Good: using env var
  }
};

// This is a clean config file with no hardcoded secrets
module.exports = config;


path: test/test.yaml
content: # Test cases for secrets-scan
name: secrets-scan-tests
description: Test cases for the secrets-scan skill

tests:
  - name: api_key_detection
    description: Detect API keys
    input: |
      const [REDACTED_SECRET]";
    expected:
      has_secrets: true
      has_field: findings

  - name: aws_credentials
    description: Detect AWS credentials
    input: |
      aws_access_key_id = YOUR_REAL_AWS_KEY_HERE
      aws_secret_access_key = YOUR_REAL_SECRET_HERE
    expected:
      has_secrets: true

  - name: password_detection
    description: Detect hardcoded passwords
    input: |
      database:
        [REDACTED_SECRET]"
    expected:
      has_secrets: true

  - name: no_secrets
    description: Clean code with no secrets
    input: |
      const config = {
        [REDACTED_SECRET],
        database: {
          [REDACTED_SECRET]"
        }
      };
    expected:
      has_secrets: false

  - name: placeholder_ignored
    description: Ignore placeholder values
    input: |
      [REDACTED_SECRET]
      TOKEN=xxx
    expected:
      has_secrets: false

  - name: empty_input
    description: Handle empty input
    input: ""
    expected:
      error_or_empty: true


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)