Skill: leefj0606/sensitive-info-protection (version 1.0.0)

--- name: sensitive-info-protection description: "Sensitive information real-time protection skill that automatically detects, alerts, and handles sensitive data in user interactions. Supports custom detection rules, external data source integration, and interactive processing. Use when you need to: (1) Scan text content for sensitive information, (2) Configure custom sensitive data detection rules, (3) Protect against accidental exposure of credentials, personal info, or business secrets." metadata: openclaw: requires: bins: - python3 --- # Sensitive Info Protection ## Overview A general-purpose sensitive information protection skill that provides real-time scanning, detection, and interactive handling of sensitive data in conversation content. It helps prevent accidental exposure of personal information, authentication credentials, and commercial secrets through configurable detection rules. ## Core Capabilities ### 1. Sensitive Information Detection - Real-time scanning of user input and output content - Detection of multiple sensitive information types by default - Support for custom user-defined detection rules with regex patterns - Priority-based rule matching ### 2. Configuration Management - Built-in default sensitive type library with common patterns - Support for adding/removing/enabling/disabling custom rules - Rule priority adjustment for conflict resolution ### 3. External Data Integration - Import sensitive keyword/pattern lists from external data sources - Support for dynamic rule updates - JSON/YAML configuration format ### 4. Interactive Processing - Standardized output format for detection results - Clear operation options for user decision-making - Support for one-click desensitization or manual editing ## Default Sensitive Types Built-in detection for the following types: - `api_key` - API keys, access tokens, authentication credentials - `credit_card` - Credit card numbers - `id_card` - National ID card numbers (Chinese) - `phone` - Mobile phone numbers (Chinese) - `email` - Email addresses - `bank_card` - Bank card numbers - `password` - Password patterns in code or logs - `secret` - Commercial secrets, confidential information markers ## Usage ### Scanning Content for Sensitive Information ```python from scripts.detector import SensitiveDetector detector = SensitiveDetector() results = detector.scan(text_content) if results: # Print detection results in standard format detector.print_results(results) # Wait for user decision to proceed else: # No sensitive information detected pass ``` ### Adding Custom Rule ```python from scripts.models import DetectionRule new_rule = DetectionRule( name="custom_secret", pattern=r"MY_SECRET=\w+", sensitivity="high", description="Custom secret pattern" ) detector.add_rule(new_rule) ``` ### Loading Configuration from File ```python detector.load_config("path/to/config.json") ``` ## Output Format When sensitive information is detected, the following format is used: ``` ## 检测结果 - 敏感类型: [type] - 位置: [start:end] - 原文: [original content] - 敏感度: [high/medium/low] ## 操作选项 1. 确认放行 2. 修改后发送 3. 取消发送 ``` ## Resources ### scripts/ - `detector.py` - Main detection engine class - `models.py` - Data models for detection rules and results - `cli.py` - Command-line interface - `default_rules.json` - Built-in default detection rules ### references/ - `configuration.md` - Detailed configuration guide - `api.md` - API documentation for integration ### assets/ - `default_config.json` - Default configuration template

path: README.md
content: # sensitive-info-protection

[English](#english) | [简体中文](#简体中文)

---

## English

A general-purpose sensitive information real-time protection Skill for OpenClaw. Automatically detects, alerts, and handles sensitive data in user interactions.

## Features

- **Real-time detection** - Scans text for multiple types of sensitive information
- **Custom rules** - Add your own detection rules with regex patterns
- **Interactive processing** - Standard output format with operation options
- **Desensitization** - One-click replacement of sensitive content
- **Built-in rules** for common sensitive types:
  - API Keys & tokens (OpenAI, GitHub, AWS)
  - Credit card, bank card numbers
  - Chinese ID card, phone numbers
  - Emails, passwords, secrets

## Installation

In OpenClaw:
```
clawhub install sensitive-info-protection
```

Or clone to your skills directory:
```
git clone https://github.com/LeeFJ0606/sensitive-info-protection.git /path/to/sensitive-info-protection
```

## Quick Start

### Python API

```python
from scripts.detector import SensitiveDetector

detector = SensitiveDetector()
result = detector.scan("My API key is [REDACTED_SECRET]")

if result.has_sensitive:
    print(result.to_markdown())
    # Get desensitized text
    clean = detector.desensitize(result.content)
    print("\nDesensitized:")
    print(clean)
```

### CLI

```bash
# Scan a file
python scripts/cli.py /path/to/file.txt

# Desensitize and output
python scripts/cli.py --desensitize /path/to/file.txt
```

## Documentation

- [Configuration Guide](references/configuration.md)
- [API Documentation](references/api.md)

## Structure

```
sensitive-info-protection/
├── SKILL.md                 # Skill definition for OpenClaw
├── scripts/
│   ├── detector.py          # Main detection engine
│   ├── models.py            # Data models
│   ├── cli.py               # Command-line interface
│   └── default_rules.json   # Built-in detection rules
├── references/
│   ├── configuration.md     # Configuration guide
│   └── api.md               # API documentation
├── assets/
│   └── default_config.json  # Default settings template
└── tests/
    └── test_basic.py        # Basic functionality tests
```

## License

MIT

---

## 简体中文

OpenClaw 通用敏感信息实时防护 Skill，可自动识别、告警并处理用户交互中的敏感数据，支持灵活配置与外部数据源接入。

## 核心功能

- **实时检测** - 监控文本内容，识别多类型敏感信息
- **自定义规则** - 支持通过正则表达式添加自定义检测规则
- **交互式处理** - 标准化输出格式，提供清晰操作选项
- **一键脱敏** - 快速替换敏感内容，防止信息泄露
- **内置规则** - 开箱即用，支持常见敏感类型：
  - API 密钥 / 访问令牌（OpenAI、GitHub、AWS 等）
  - 信用卡号、银行卡号
  - 中国身份证号、手机号码
  - 电子邮箱、密码、机密信息

## 安装

在 OpenClaw 中使用：
```
clawhub install sensitive-info-protection
```

或者直接克隆到你的 Skills 目录：
```
git clone https://github.com/LeeFJ0606/sensitive-info-protection.git /path/to/sensitive-info-protection
```

## 快速开始

### Python API

```python
from scripts.detector import SensitiveDetector

detector = SensitiveDetector()
result = detector.scan("我的 API 密钥是 [REDACTED_SECRET]")

if result.has_sensitive:
    print(result.to_markdown())  # 输出标准检测结果
    # 获取脱敏后的文本
    clean = detector.desensitize(result.content)
    print("\n脱敏结果：")
    print(clean)
```

### 命令行使用

```bash
# 扫描文件
python scripts/cli.py /path/to/file.txt

# 输出脱敏结果
python scripts/cli.py --desensitize /path/to/file.txt
```

## 文档

- [配置指南](references/configuration.md)
- [API 文档](references/api.md)

## 项目结构

```
sensitive-info-protection/
├── SKILL.md                 # OpenClaw Skill 定义
├── scripts/
│   ├── detector.py          # 核心检测引擎
│   ├── models.py            # 数据模型定义
│   ├── cli.py               # 命令行接口
│   └── default_rules.json   # 内置检测规则
├── references/
│   ├── configuration.md     # 详细配置指南
│   └── api.md               # API 文档
├── assets/
│   └── default_config.json  # 默认配置模板
└── tests/
    └── test_basic.py        # 基础功能测试
```

## 许可证

MIT


path: assets/default_config.json
content: {
  "settings": {
    "block_on_high": true,
    "warn_on_medium": true,
    "ignore_low": false,
    "default_replacement": "***"
  },
  "custom_rules": []
}


path: assets/sensitive-interaction.js
content: /**
 * sensitive-info-protection 交互按钮组件
 * 适配 OpenClaw Lit 控制 UI webchat
 */

function injectSensitiveButtons(node, content) {
  // 查找操作选项区域
  const optionsMatch = content.match(/## 操作选项\n([\s\S]*)/);
  if (!optionsMatch) return;

  // 创建按钮容器
  const buttonsDiv = document.createElement('div');
  buttonsDiv.className = 'sensitive-action-buttons';
  buttonsDiv.style.marginTop = '12px';
  buttonsDiv.style.display = 'flex';
  buttonsDiv.style.gap = '8px';
  buttonsDiv.style.paddingBottom = '8px';

  // 定义按钮
  const actions = [
    { text: '1. 确认放行', value: 'confirm', style: 'background: #4CAF50; color: white;' },
    { text: '2. 修改后发送', value: 'edit', style: 'background: #FF9800; color: white;' },
    { text: '3. 取消发送', value: 'cancel', style: 'background: #f44336; color: white;' }
  ];

  actions.forEach(action => {
    const button = document.createElement('button');
    button.textContent = action.text;
    button.style = `
      padding: 8px 16px;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
      ${action.style}
    `;
    button.onclick = () => handleAction(action.value);
    buttonsDiv.appendChild(button);
  });

  // 适配 OpenClaw Lit 组件 DOM 结构
  // 直接添加到当前检测结果消息节点内部末尾
  if (node.shadowRoot) {
    // 如果是 Web Component，查找消息内容区域
    const contentArea = node.shadowRoot.querySelector('[class*="message"], [part*="message"], div:last-child');
    if (contentArea) {
      contentArea.appendChild(buttonsDiv);
      return;
    }
  }
  
  node.appendChild(buttonsDiv);
}

function handleAction(action) {
  // OpenClaw 控制 UI class 选择器适配
  const inputBox = document.querySelector('textarea, [class*="input"], [part*="input"]');
  switch(action) {
    case 'confirm':
      submitMessage('确认放行', inputBox);
      break;
    case 'edit':
      // 弹出修改框
      const originalText = window.lastSensitiveContent || '';
      const newText = prompt('请输入修改后的内容：', originalText);
      if (newText !== null && newText.trim() !== '') {
        submitMessage(newText, inputBox);
      }
      break;
    case 'cancel':
      submitMessage('取消发送', inputBox);
      break;
  }
}

function submitMessage(text, inputBox) {
  if (inputBox) {
    // 尝试多种方式设置值
    if ('value' in inputBox) {
      inputBox.value = text;
    } else if (inputBox.firstChild && 'value' in inputBox.firstChild) {
      inputBox.firstChild.value = text;
    }
    
    // 触发 input 事件让框架感知变化
    const event = new Event('input', { bubbles: true });
    inputBox.dispatchEvent(event);
    
    // 触发发送按钮点击，适配多种选择器
    setTimeout(() => {
      const sendButton = document.querySelector('button[type="submit"], [class*="send"], [part*="send"]');
      if (sendButton) {
        sendButton.click();
      }
    }, 100);
  }
}

// 自动检测：当新消息包含敏感检测结果时注入按钮
function observeNewMessages() {
  const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
      if (mutation.addedNodes.length) {
        mutation.addedNodes.forEach(node => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            // 检查节点和所有子节点内容
            let content = node.textContent || '';
            if (!content.includes('## 检测结果') || !content.includes('## 操作选项')) {
              // 检查 shadow DOM
              if (node.shadowRoot) {
                content = node.shadowRoot.textContent || '';
              }
            }
            
            if (content.includes('## 检测结果') && content.includes('## 操作选项')) {
              // 保存最后敏感内容供编辑使用
              const match = content.match(/原文: `(.*?)`/);
              if (match) {
                window.lastSensitiveContent = match[1];
              }
              // 注入按钮到当前节点
              injectSensitiveButtons(node, content);
            }
          }
        });
      }
    });
  });

  // 查找顶级容器，适配 OpenClaw 结构
  const chatContainer = document.querySelector('openclaw-app, .chat-container, main, [role="log"]');
  if (chatContainer) {
    observer.observe(chatContainer, { childList: true, subtree: true });
  } else {
    // fallback 到 document body
    observer.observe(document.body, { childList: true, subtree: true });
  }
}

// 页面加载完成后初始化
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', observeNewMessages);
} else {
  // 延迟一点初始化，确保 Lit 组件已经渲染
  setTimeout(observeNewMessages, 500);
}



path: references/api.md
content: # API Documentation

## Class: SensitiveDetector

### Constructor

```python
SensitiveDetector()
```

Creates a new detector instance with default rules loaded automatically.

### Methods

#### `scan(content: str) -> ScanResult`
Scan the given content for sensitive information.

**Parameters:**
- `content`: The text content to scan

**Returns:** `ScanResult` object containing detection results

---

#### `add_rule(rule: DetectionRule) -> None`
Add a new detection rule to the detector. Automatically recompiles and re-sorts rules by priority.

**Parameters:**
- `rule`: `DetectionRule` object to add

---

#### `remove_rule(rule_name: str) -> bool`
Remove a rule by name.

**Parameters:**
- `rule_name`: Name of the rule to remove

**Returns:** `True` if rule was removed, `False` if not found

---

#### `enable_rule(rule_name: str) -> bool`
Enable a previously disabled rule.

**Parameters:**
- `rule_name`: Name of the rule to enable

**Returns:** `True` if rule was found and enabled, `False` if not found

---

#### `disable_rule(rule_name: str) -> bool`
Disable a rule by name (keeps it in the list but doesn't scan with it).

**Parameters:**
- `rule_name`: Name of the rule to disable

**Returns:** `True` if rule was found and disabled, `False` if not found

---

#### `load_config(config_path: str) -> None`
Load additional rules from a JSON configuration file.

**Parameters:**
- `config_path`: Path to JSON configuration file

**Raises:** `ValueError` if file format is not supported

---

#### `desensitize(content: str, replacement: str = "***") -> str`
Replace all detected sensitive information with replacement text.

**Parameters:**
- `content`: Original text content
- `replacement`: Text to replace sensitive content with (default: `"***"`)

**Returns:** Desensitized text string

---

#### `print_results(result: ScanResult) -> None`
Print detection results in the standard markdown format.

**Parameters:**
- `result`: `ScanResult` from `scan()`

---

## Class: DetectionRule

Represents a single detection rule.

```python
DetectionRule(
    name: str,
    pattern: str,
    sensitivity: str = "medium",
    description: str = "",
    enabled: bool = True,
    priority: int = 0
)
```

**Methods:**
- `compile() -> None`: Compile the regex pattern (called automatically)
- `match(text: str) -> List[re.Match]`: Find all matches in text

## Class: DetectionResult

Represents a single detection match.

**Attributes:**
- `rule`: The `DetectionRule` that matched
- `match`: The regex `Match` object
- `start`: Start position in text
- `end`: End position in text
- `text`: The matched text

**Methods:**
- `to_dict() -> dict`: Convert to dictionary

## Class: ScanResult

Represents the complete scan result for a text.

**Attributes:**
- `content`: The original scanned content
- `detections`: List of `DetectionResult` objects
- `has_sensitive`: `True` if any sensitive information was found

**Methods:**
- `add_detection(detection: DetectionResult) -> None`: Add a detection
- `sort_by_priority() -> None`: Sort detections by priority/sensitivity
- `to_markdown() -> str`: Format as markdown for display


path: references/configuration.md
content: # Configuration Guide

## Rule Format

Each detection rule is defined as a JSON object with the following fields:

| Field          | Type    | Required | Default | Description |
|----------------|---------|----------|---------|-------------|
| `name`         | string  | Yes      | -       | Unique rule name |
| `pattern`      | string  | Yes      | -       | Regular expression pattern |
| `sensitivity`  | string  | No       | medium  | One of: `high`, `medium`, `low` |
| `description`  | string  | No       | ""      | Human-readable description |
| `enabled`      | boolean | No       | true    | Whether the rule is enabled |
| `priority`     | integer | No       | 0       | Higher priority rules match first |

## Example Configuration

```json
[
  {
    "name": "company_internal_id",
    "pattern": "INT-[0-9]{6}-[A-Z]{2}",
    "sensitivity": "high",
    "description": "Company internal document ID",
    "priority": 8
  },
  {
    "name": "internal_url",
    "pattern": "https://internal\\.company\\.com/[a-zA-Z0-9-_]+",
    "sensitivity": "medium",
    "description": "Internal company URL",
    "enabled": true
  }
]
```

## Regex Tips

- Use word boundaries `\b` to avoid partial matches
- Escape special characters with double backslash: `\\b`, `\\d`
- Test your regex patterns before adding them to the configuration

## Loading Configuration

### Python API

```python
from sensitive_info_protection import SensitiveDetector

detector = SensitiveDetector()
detector.load_config('/path/to/your/custom-rules.json')
```

### CLI

```bash
python scripts/cli.py --config /path/to/config.json input.txt
```

## Management Commands

### Disable a built-in rule

```python
detector.disable_rule('phone')
```

### Enable a disabled rule

```python
detector.enable_rule('phone')
```

### Remove a rule completely

```python
detector.remove_rule('email')
```

### Add a custom rule at runtime

```python
from sensitive_info_protection.models import DetectionRule

rule = DetectionRule(
    name="my_secret",
    pattern="MY_SECRET=\\w+",
    sensitivity="high"
)
detector.add_rule(rule)
```


path: scripts/cli.py
content: #!/usr/bin/env python3
import argparse
import sys
from detector import SensitiveDetector


def main():
    parser = argparse.ArgumentParser(description='Sensitive Information Detection CLI')
    parser.add_argument('input', nargs='?', help='Input text or file to scan (use - for stdin)')
    parser.add_argument('--config', '-c', help='Path to custom rules configuration file')
    parser.add_argument('--desensitize', '-d', action='store_true', help='Output desensitized text')
    parser.add_argument('--replacement', '-r', default='***', help='Replacement text for desensitization')

    args = parser.parse_args()

    # Initialize detector
    detector = SensitiveDetector()

    # Load custom config if provided
    if args.config:
        detector.load_config(args.config)

    # Get input content
    if not args.input:
        print("No input provided. Reading from stdin...")
        content = sys.stdin.read()
    elif args.input == '-':
        content = sys.stdin.read()
    else:
        try:
            with open(args.input, 'r', encoding='utf-8') as f:
                content = f.read()
        except FileNotFoundError:
            content = args.input

    # Scan
    result = detector.scan(content)

    if args.desensitize:
        if result.has_sensitive:
            desensitized = detector.desensitize(content, args.replacement)
            print(desensitized)
        else:
            print(content)
    else:
        print(result.to_markdown())

    if result.has_sensitive and not args.desensitize:
        sys.exit(1)
    sys.exit(0)


if __name__ == '__main__':
    main()


path: scripts/default_rules.json
content: name: api_key
pattern: (sk|pk|tk|token|key|access)[_-]?[0-9a-zA-Z]{20,}
sensitivity: high
description: API Key or Access Token
priority: 10

name: openai_key
pattern: sk-[0-9a-zA-Z]{48}
sensitivity: high
description: OpenAI API Key
priority: 15

name: aws_access_key
pattern: AKIA[0-9A-Z]{16}
sensitivity: high
description: AWS Access Key ID
priority: 15

name: credit_card
pattern: \b(?:\d{4}[- ]?){3}\d{4}\b
sensitivity: high
description: Credit Card Number
priority: 10

name: id_card
pattern: [1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]
sensitivity: high
description: Chinese ID Card Number
priority: 10

name: phone
pattern: 1[3-9]\d{9}
sensitivity: medium
description: Chinese Mobile Phone Number
priority: 5

name: email
pattern: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
sensitivity: medium
description: Email Address
priority: 5

name: bank_card
pattern: \b\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4}\b
sensitivity: high
description: Bank Card Number
priority: 10

name: password
pattern: (password|passwd|pwd|secret)[ :=]+['"]?[\w]{6,}['"]?
sensitivity: high
description: Password in text/code
priority: 10

name: github_token
pattern: gh[ps]_[A-Za-z0-9]{36,}
sensitivity: high
description: GitHub Personal Access Token
priority: 15

path: scripts/detector.py
content: import json
import os
from typing import List, Optional
from models import DetectionRule, DetectionResult, ScanResult


class SensitiveDetector:
    """Main sensitive information detection engine"""

    def __init__(self):
        self.rules: List[DetectionRule] = []
        self._load_default_rules()

    def _load_default_rules(self) -> None:
        """Load built-in default rules"""
        script_dir = os.path.dirname(os.path.abspath(__file__))
        default_rules_path = os.path.join(script_dir, 'default_rules.json')

        if os.path.exists(default_rules_path):
            with open(default_rules_path, 'r', encoding='utf-8') as f:
                rules_data = json.load(f)
                for rule_data in rules_data:
                    rule = DetectionRule(**rule_data)
                    rule.compile()
                    self.rules.append(rule)

        # Sort by priority
        self.rules.sort(key=lambda x: -x.priority)

    def load_config(self, config_path: str) -> None:
        """Load rules from configuration file"""
        with open(config_path, 'r', encoding='utf-8') as f:
            if config_path.endswith('.json'):
                rules_data = json.load(f)
            else:
                raise ValueError("Only JSON configuration is supported currently")

        for rule_data in rules_data:
            rule = DetectionRule(**rule_data)
            rule.compile()
            self.rules.append(rule)

        # Re-sort by priority
        self.rules.sort(key=lambda x: -x.priority)

    def add_rule(self, rule: DetectionRule) -> None:
        """Add a new detection rule"""
        rule.compile()
        self.rules.append(rule)
        self.rules.sort(key=lambda x: -x.priority)

    def remove_rule(self, rule_name: str) -> bool:
        """Remove a rule by name"""
        original_count = len(self.rules)
        self.rules = [r for r in self.rules if r.name != rule_name]
        return len(self.rules) < original_count

    def enable_rule(self, rule_name: str) -> bool:
        """Enable a rule by name"""
        for rule in self.rules:
            if rule.name == rule_name:
                rule.enabled = True
                return True
        return False

    def disable_rule(self, rule_name: str) -> bool:
        """Disable a rule by name"""
        for rule in self.rules:
            if rule.name == rule_name:
                rule.enabled = False
                return True
        return False

    def scan(self, content: str) -> ScanResult:
        """Scan content for sensitive information"""
        result = ScanResult(content=content)

        for rule in self.rules:
            if not rule.enabled:
                continue

            matches = rule.match(content)
            for match in matches:
                detection = DetectionResult(
                    rule=rule,
                    match=match,
                    start=match.start(),
                    end=match.end(),
                    text=match.group()
                )
                result.add_detection(detection)

        result.sort_by_priority()
        return result

    def desensitize(self, content: str, replacement: str = "***") -> str:
        """Desensitize all detected sensitive information"""
        result = self.scan(content)
        if not result.has_sensitive:
            return content

        # Sort detections by start position descending to avoid offset issues
        sorted_detections = sorted(result.detections, key=lambda x: -x.start)
        content_list = list(content)

        for detection in sorted_detections:
            # Replace the sensitive text with replacement
            del content_list[detection.start:detection.end]
            content_list.insert(detection.start, replacement)

        return ''.join(content_list)

    def print_results(self, result: ScanResult) -> None:
        """Print detection results in standard format"""
        print(result.to_markdown())


path: scripts/models.py
content: from dataclasses import dataclass, field
from typing import List, Optional
import re


@dataclass
class DetectionRule:
    """Detection rule for sensitive information"""
    name: str
    pattern: str
    sensitivity: str = "medium"  # high, medium, low
    description: str = ""
    enabled: bool = True
    priority: int = 0  # Higher priority rules match first
    compiled: Optional[re.Pattern] = None

    def compile(self) -> None:
        """Compile the regex pattern"""
        if self.compiled is None:
            self.compiled = re.compile(self.pattern)

    def match(self, text: str) -> List[re.Match]:
        """Find all matches in text"""
        if not self.enabled:
            return []
        if self.compiled is None:
            self.compile()
        return list(self.compiled.finditer(text))


@dataclass
class DetectionResult:
    """Result of a single sensitive detection"""
    rule: DetectionRule
    match: re.Match
    start: int
    end: int
    text: str

    def to_dict(self) -> dict:
        """Convert to dictionary"""
        return {
            "sensitive_type": self.rule.name,
            "start": self.start,
            "end": self.end,
            "text": self.text,
            "sensitivity": self.rule.sensitivity,
            "description": self.rule.description
        }


@dataclass
class ScanResult:
    """Result of scanning a text"""
    content: str
    detections: List[DetectionResult] = field(default_factory=list)
    has_sensitive: bool = False

    def add_detection(self, detection: DetectionResult) -> None:
        """Add a detection result"""
        self.detections.append(detection)
        self.has_sensitive = True

    def sort_by_priority(self) -> None:
        """Sort detections by rule priority and sensitivity"""
        sensitivity_order = {"high": 3, "medium": 2, "low": 1}
        self.detections.sort(
            key=lambda x: (-x.rule.priority, -sensitivity_order.get(x.rule.sensitivity, 0))
        )

    def to_markdown(self) -> str:
        """Format result as markdown for display"""
        if not self.has_sensitive:
            return "No sensitive information detected."

        output = ["## 检测结果"]
        for i, det in enumerate(self.detections, 1):
            output.append(f"- 敏感类型: {det.rule.name}")
            output.append(f"- 位置: {det.start}:{det.end}")
            output.append(f"- 原文: `{det.text}`")
            output.append(f"- 敏感度: {det.rule.sensitivity}")
            if det.rule.description:
                output.append(f"- 描述: {det.rule.description}")
            if i < len(self.detections):
                output.append("")

        output.append("\n## 操作选项")
        output.append("1. 确认放行")
        output.append("2. 修改后发送")
        output.append("3. 取消发送")

        return "\n".join(output)


path: tests/test_basic.py
content: #!/usr/bin/env python3
"""Basic tests for sensitive-info-protection skill"""

import sys
import os
scripts_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'scripts')
sys.path.insert(0, scripts_path)

from detector import SensitiveDetector


def test_api_key_detection():
    """Test API key detection"""
    detector = SensitiveDetector()
    text = "My OpenAI key is [REDACTED_SECRET]"
    result = detector.scan(text)
    assert result.has_sensitive
    assert any(d.rule.name == "openai_key" for d in result.detections)
    print("✓ API key detection passed")


def test_github_token_detection():
    """Test GitHub token detection"""
    detector = SensitiveDetector()
    text = "github_token: [REDACTED_SECRET]"
    result = detector.scan(text)
    assert result.has_sensitive
    assert any(d.rule.name == "github_token" for d in result.detections)
    print("✓ GitHub token detection passed")


def test_phone_detection():
    """Test Chinese phone number detection"""
    detector = SensitiveDetector()
    text = "我的手机号是13800138000"
    result = detector.scan(text)
    assert result.has_sensitive
    assert any(d.rule.name == "phone" for d in result.detections)
    print("✓ Phone number detection passed")


def test_email_detection():
    """Test email detection"""
    detector = SensitiveDetector()
    text = "联系我 at [REDACTED_SECRET]"
    result = detector.scan(text)
    assert result.has_sensitive
    assert any(d.rule.name == "email" for d in result.detections)
    print("✓ Email detection passed")


def test_desensitization():
    """Test desensitization works correctly"""
    detector = SensitiveDetector()
    text = "My key is [REDACTED_SECRET] and phone is 13800138000"
    desensitized = detector.desensitize(text)
    assert "sk-" not in desensitized
    assert "13800138000" not in desensitized
    assert "***" in desensitized
    print("✓ Desensitization passed")


def test_disable_enable_rule():
    """Test disabling and enabling rules"""
    detector = SensitiveDetector()
    text = "[REDACTED_SECRET]"

    # Original detection
    result = detector.scan(text)
    assert result.has_sensitive

    # Disable email detection
    success = detector.disable_rule("email")
    assert success
    result = detector.scan(text)
    assert not result.has_sensitive

    # Enable it back
    success = detector.enable_rule("email")
    assert success
    result = detector.scan(text)
    assert result.has_sensitive
    print("✓ Disable/enable rule passed")


def test_add_custom_rule():
    """Test adding a custom rule"""
    from models import DetectionRule
    detector = SensitiveDetector()

    # Add custom rule for secret pattern
    rule = DetectionRule(
        name="custom_secret",
        pattern="MY_SECRET=\\w+",
        sensitivity="high"
    )
    detector.add_rule(rule)

    text = "MY_SECRET=supersecret123"
    result = detector.scan(text)
    assert result.has_sensitive
    assert any(d.rule.name == "custom_secret" for d in result.detections)
    print("✓ Add custom rule passed")


def test_no_false_positive():
    """Test that normal text doesn't get false positives"""
    detector = SensitiveDetector()
    text = "Hello world! This is a normal message with no sensitive information."
    result = detector.scan(text)
    assert not result.has_sensitive
    print("✓ No false positive passed")


def main():
    """Run all tests"""
    print("Running sensitive-info-protection basic tests...\n")

    tests = [
        test_api_key_detection,
        test_github_token_detection,
        test_phone_detection,
        test_email_detection,
        test_desensitization,
        test_disable_enable_rule,
        test_add_custom_rule,
        test_no_false_positive
    ]

    passed = 0
    failed = 0

    for test in tests:
        try:
            test()
            passed += 1
        except Exception as e:
            print(f"✗ {test.__name__} failed: {e}")
            failed += 1

    print(f"\n{'='*40}")
    print(f"Total: {passed + failed} tests")
    print(f"Passed: {passed}")
    print(f"Failed: {failed}")

    if failed > 0:
        sys.exit(1)
    else:
        print("\nAll tests passed! ✓")
        sys.exit(0)


if __name__ == "__main__":
    main()


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