Skill: aronchick/expanso-pii-detect (version 1.0.0)

# pii-detect Detect personally identifiable information (PII) in text ## 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/pii-detect/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: # pii-detect

> Detect personally identifiable information (PII) in text.

Scan text for sensitive personal data before processing, logging, or sharing. Essential for:
- GDPR/CCPA compliance
- Log sanitization before storage
- Data loss prevention (DLP)
- Privacy-aware data pipelines

## Quick Start

### CLI Mode

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

# Scan text for all PII types
echo "Contact John Smith at [REDACTED_SECRET] or 555-123-4567" | \
  expanso-edge run pipeline-cli.yaml

# Scan for specific types only
echo "SSN: 123-45-6789, Card: 4111-1111-1111-1111" | \
  PII_TYPES="ssn,credit_card" expanso-edge run pipeline-cli.yaml
```

### MCP Mode

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

curl -X POST http://localhost:8080/detect \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Please contact [REDACTED_SECRET] or call 555-987-6543",
    "types": ["email", "phone"]
  }'
```

## Configuration

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

## PII Types

| Type | Description | Examples |
|------|-------------|----------|
| `email` | Email addresses | [REDACTED_SECRET] |
| `phone` | Phone numbers | 555-123-4567, +1 (555) 123-4567 |
| `ssn` | Social Security Numbers | 123-45-6789 |
| `credit_card` | Credit card numbers | 4111-1111-1111-1111 |
| `name` | Person names | John Smith, Jane Doe |
| `address` | Physical addresses | 123 Main St, New York, NY |
| `dob` | Dates of birth | 01/15/1990, January 15, 1990 |
| `ip_address` | IP addresses | 192.168.1.1, 10.0.0.1 |

## Example Output

### Input
```
Customer Support Ticket #12345

From: John Smith <[REDACTED_SECRET]>
Phone: (555) 123-4567
SSN: 123-45-6789

Please update my address to 123 Main Street, Boston, MA 02101.
My date of birth is March 15, 1985.
```

### Output
```json
{
  "findings": [
    {"type": "name", "value": "John Smith", "start": 35, "end": 45, "confidence": 0.95},
    {"type": "email", "value": "[REDACTED_SECRET]", "start": 47, "end": 69, "confidence": 1.0},
    {"type": "phone", "value": "(555) 123-4567", "start": 78, "end": 92, "confidence": 1.0},
    {"type": "ssn", "value": "123-45-6789", "start": 99, "end": 110, "confidence": 1.0},
    {"type": "address", "value": "123 Main Street, Boston, MA 02101", "start": 140, "end": 173, "confidence": 0.9},
    {"type": "dob", "value": "March 15, 1985", "start": 200, "end": 214, "confidence": 0.95}
  ],
  "has_pii": true,
  "summary": "Found 6 PII items: 1 name, 1 email, 1 phone, 1 SSN, 1 address, 1 DOB",
  "metadata": {
    "skill": "pii-detect",
    "input_hash": "abc123...",
    "trace_id": "550e8400-...",
    "findings_count": 6
  }
}
```

## Use Cases

### Pre-Logging Check
```bash
# Check logs before shipping to external service
cat application.log | expanso-edge run pipeline-cli.yaml | jq '.has_pii'
```

### Compliance Scanning
```bash
# Scan all customer data files
for f in customer_*.json; do
  echo "Scanning $f..."
  cat "$f" | expanso-edge run pipeline-cli.yaml
done
```

### Pipeline Integration
```yaml
# Use in a larger Expanso pipeline
pipeline:
  processors:
    - subprocess:
        name: pii-check
        command: ["expanso-edge", "run", "skills/pii-detect/pipeline-cli.yaml"]
    - switch:
        - check: this.has_pii == true
          processors:
            - log:
                level: WARN
                message: "PII detected - routing to secure storage"
```

## Related Skills

- [pii-redact](../pii-redact/) - Redact detected PII from text
- [secrets-scan](../secrets-scan/) - Detect hardcoded secrets
- [log-sanitize](../log-sanitize/) - Sanitize logs before storage

---

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


path: pipeline-cli.yaml
content: # Expanso Pipeline: pii-detect (CLI mode)
# ==========================================
#
# Detect personally identifiable information (PII) in text.
#
# Usage:
#   echo "Contact John at [REDACTED_SECRET] or 555-123-4567" | \
#     expanso-edge run pipeline-cli.yaml
#
#   # Specific PII types only
#   echo "SSN: 123-45-6789" | PII_TYPES="ssn,credit_card" expanso-edge run pipeline-cli.yaml

name: pii-detect-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 pii_types = env("PII_TYPES").or("email,phone,ssn,credit_card,name,address,dob,ip_address")

          root.messages = [
            {
              "role": "system",
              "content": "You are a PII detection specialist. Scan text for personally identifiable information and return structured findings. Be thorough but avoid false positives. Only report actual PII, not generic references."
            },
            {
              "role": "user",
              "content": "Scan this text for the following PII types: " + $pii_types + "\n\nReturn a JSON object:\n{\n  \"findings\": [\n    {\"type\": \"email|phone|ssn|credit_card|name|address|dob|ip_address\", \"value\": \"the actual PII\", \"start\": 0, \"end\": 0, \"confidence\": 0.0-1.0}\n  ],\n  \"summary\": \"Brief summary of what was found\"\n}\n\nIf no PII is found, return {\"findings\": [], \"summary\": \"No PII detected\"}.\nReturn valid JSON only.\n\nText to scan:\n" + content()
            }
          ]

      - 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_pii = $parsed.findings.or([]).length() > 0
          root.summary = $parsed.summary.or("")
          root.metadata = {
            "skill": "pii-detect",
            "mode": "cli",
            "model": "gpt-4o-mini",
            "input_hash": meta("input_hash"),
            "input_length": meta("input_length"),
            "trace_id": meta("trace_id"),
            "pii_types_scanned": env("PII_TYPES").or("all"),
            "findings_count": $parsed.findings.or([]).length(),
            "timestamp": now()
          }

      - log:
          level: INFO
          message: |
            [pii-detect] Found ${! root.findings.length() } PII items in ${! meta("input_length") } chars (trace: ${! meta("trace_id").slice(0, 8) })

  output:
    stdout:
      codec: json_object


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

name: pii-detect-mcp
type: pipeline

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

  input:
    http_server:
      path: /detect
      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 pii_types = this.types.or(["email", "phone", "ssn", "credit_card", "name", "address"]).join(",")

          root.messages = [
            {
              "role": "system",
              "content": "You are a PII detection specialist. Scan text for personally identifiable information and return structured findings. Be thorough but avoid false positives."
            },
            {
              "role": "user",
              "content": "Scan this text for the following PII types: " + $pii_types + "\n\nReturn a JSON object:\n{\n  \"findings\": [\n    {\"type\": \"...\", \"value\": \"the actual PII\", \"start\": 0, \"end\": 0, \"confidence\": 0.0-1.0}\n  ],\n  \"summary\": \"Brief summary\"\n}\n\nReturn valid JSON only.\n\nText:\n" + $text
            }
          ]

      - 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_pii = $parsed.findings.or([]).length() > 0
          root.summary = $parsed.summary.or("")
          root.metadata = {
            "skill": "pii-detect",
            "mode": "mcp",
            "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: |
            [pii-detect] Found ${! root.findings.length() } PII items (trace: ${! meta("trace_id").slice(0, 8) })

  output:
    sync_response: {}


path: skill.yaml
content: # Skill: pii-detect
# Version: 1.0.0
#
# Detect personally identifiable information (PII) in text.

name: pii-detect
version: 1.0.0
description: Detect personally identifiable information (PII) in text

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

credentials:
  - name: OPENAI_API_KEY
    required: false
    description: OpenAI API key (optional - can use regex patterns only)

inputs:
  - name: text
    type: string
    required: true
    description: Text to scan for PII
  - name: types
    type: array
    required: false
    description: PII types to detect (email, phone, ssn, credit_card, etc.)

outputs:
  - name: findings
    type: array
    description: Detected PII with type, value, and position
  - name: has_pii
    type: boolean
    description: Whether any PII was found
  - name: metadata
    type: object
    description: Audit metadata

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

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


path: test/fixtures/input.txt
content: Customer Support Ticket #12345

From: John Smith
Email: [REDACTED_SECRET]
Phone: (555) 123-4567

Please update my shipping address to:
123 Main Street
Boston, MA 02101

My date of birth is March 15, 1985.


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

tests:
  - name: email_detection
    description: Detect email addresses
    input: "Contact me at [REDACTED_SECRET]"
    expected:
      has_field: findings
      has_pii: true

  - name: phone_detection
    description: Detect phone numbers
    input: "Call us at 555-123-4567 or (800) 555-1234"
    expected:
      has_field: findings
      has_pii: true

  - name: ssn_detection
    description: Detect Social Security Numbers
    input: "SSN: 123-45-6789"
    env:
      PII_TYPES: ssn
    expected:
      has_pii: true

  - name: no_pii
    description: Text with no PII
    input: "The weather is nice today. Let's go for a walk."
    expected:
      has_pii: false
      findings_length: 0

  - name: multiple_pii
    description: Text with multiple PII types
    input: "John Smith ([REDACTED_SECRET], 555-1234) lives at 123 Main St"
    expected:
      has_pii: true

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


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