Skill: 52yuanchangxing/permission-footprint-reviewer (version 1.0.0)

--- name: permission-footprint-reviewer version: 1.0.0 description: "梳理某个 Skill、脚本或工作流需要的权限，并提出最小权限替代方案。；use for permissions, least-privilege, security workflows；do not use for 绕过系统安全控制, 生成提权方法." author: OpenClaw Skill Bundle homepage: https://example.invalid/skills/permission-footprint-reviewer tags: [permissions, least-privilege, security, audit] user-invocable: true metadata: {"openclaw":{"emoji":"🪪","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}} --- # 权限足迹审查员 ## 你是什么 你是“权限足迹审查员”这个独立 Skill，负责：梳理某个 Skill、脚本或工作流需要的权限，并提出最小权限替代方案。 ## Routing ### 适合使用的情况 - 审查这个脚本需要哪些权限 - 给我最小权限方案 - 输入通常包含：脚本说明、权限需求、运行环境 - 优先产出：当前权限足迹、用途说明、复核清单 ### 不适合使用的情况 - 不要绕过系统安全控制 - 不要生成提权方法 - 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置，先明确边界，再只给审阅版内容或 dry-run 方案。 ## 工作规则 1. 先把用户提供的信息重组成任务书，再输出结构化结果。 2. 缺信息时，优先显式列出“待确认项”，而不是直接编造。 3. 默认先给“可审阅草案”，再给“可执行清单”。 4. 遇到高风险、隐私、权限或合规问题，必须加上边界说明。 5. 如运行环境允许 shell / exec，可使用： - `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>` 6. 如当前环境不能执行脚本，仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。 ## 标准输出结构 请尽量按以下结构组织结果： - 当前权限足迹 - 用途说明 - 过度权限 - 最小权限替代 - 隔离建议 - 复核清单 ## 本地资源 - 规范文件：`{baseDir}/resources/spec.json` - 输出模板：`{baseDir}/resources/template.md` - 示例输入输出：`{baseDir}/examples/` - 冒烟测试：`{baseDir}/tests/smoke-test.md` ## 安全边界 - 只做审计和最小化建议。 - 默认只读、可审计、可回滚。 - 不执行高风险命令，不隐藏依赖，不伪造事实或结果。

path: README.md
content: # 权限足迹审查员

## 功能
梳理某个 Skill、脚本或工作流需要的权限，并提出最小权限替代方案。

## 适用场景
- 权限评审
- 技能审查
- 自动化治理

## 推荐实现边界
- 模式：`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入：脚本说明、权限需求、运行环境
- 输出：以 Markdown 为主，强调可审阅、可追踪、可补充。
- 风险控制：只做审计和最小化建议。

## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用

## 目录结构
- `SKILL.md`：Skill 说明与路由规则
- `README.md`：功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`：本 Skill 的规范与质量自检
- `scripts/run.py`：本地可执行脚本，负责生成或审计结果
- `resources/spec.json`：结构化配置，驱动脚本与模板
- `resources/template.md`：输出模板
- `examples/example-input.md`：示例输入
- `examples/example-output.md`：示例输出
- `tests/smoke-test.md`：冒烟测试步骤

## 触发示例
- 审查这个脚本需要哪些权限
- 给我最小权限方案

## 输入输出示例
### 输入侧重点
- 当前权限足迹
- 用途说明
- 过度权限

### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```

### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议

## 脚本参数
```text
--input   输入文件或目录
--output  输出文件，默认 stdout
--format  markdown/json，默认 markdown
--limit   限制扫描或摘要数量
--dry-run 仅分析不写文件
```

## 常见问题
**问：这个 Skill 会直接修改外部系统吗？**  不会，默认只生成草案、清单或只读审计结果。
**问：没有 shell/exec 工具还能用吗？**  可以，Skill 会直接按模板产出文本结果。
**问：脚本依赖什么？**  只依赖 `python3` 和 Python 标准库。

## 风险提示
- 仅使用本地输入内容，不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料，建议先脱敏再处理。


path: SELF_CHECK.md
content: # 权限足迹审查员 自检

| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata，metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库，已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式，不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流，门槛低，可二次定制。 |
| 可维护性 | 通过 | 结构统一，资源驱动，便于版本升级和批量修订。 |

## 评分
- 综合评分：96/100
- 扣分点：暂无阻断项；后续可按真实用户反馈再细化例子和模板。

## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。

path: examples/example-input.md
content: # 权限足迹审查员 示例输入

目标：权限评审
输入类型：脚本说明、权限需求、运行环境

## 背景
- 这是一个用于演示 权限足迹审查员 的最小可复核样例。
- 希望产出与“当前权限足迹 / 用途说明 / 复核清单”相关的结构化结果。

## 原始材料
- 主题：权限足迹审查员 场景演示
- 约束：时间有限，需要先产出审阅版，再决定是否落地。
- 风险：不允许编造事实，不允许直接执行高风险动作。

## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。


path: examples/example-output.md
content: # 权限足迹审查员 示例输出

## 当前权限足迹
- 这里是与“当前权限足迹”相关的示例条目。

## 用途说明
- 这里是与“用途说明”相关的示例条目。

## 过度权限
- 这里是与“过度权限”相关的示例条目。

## 最小权限替代
- 这里是与“最小权限替代”相关的示例条目。

## 隔离建议
- 这里是与“隔离建议”相关的示例条目。

## 复核清单
- 这里是与“复核清单”相关的示例条目。

## 待确认项
- 这里列出仍需用户补充的信息。

## 下一步
- 在用户确认后，再进入执行或二次加工。

path: resources/spec.json
content: {
  "slug": "permission-footprint-reviewer",
  "title": "权限足迹审查员",
  "category": "security",
  "categoryLabel": "安全与治理",
  "mode": "structured_brief",
  "summary": "梳理某个 Skill、脚本或工作流需要的权限，并提出最小权限替代方案。",
  "inputHint": "脚本说明、权限需求、运行环境",
  "sections": [
    "当前权限足迹",
    "用途说明",
    "过度权限",
    "最小权限替代",
    "隔离建议",
    "复核清单"
  ],
  "useCases": [
    "权限评审",
    "技能审查",
    "自动化治理"
  ],
  "positiveExamples": [
    "审查这个脚本需要哪些权限",
    "给我最小权限方案"
  ],
  "negativeExamples": [
    "不要绕过系统安全控制",
    "不要生成提权方法"
  ],
  "risk": "只做审计和最小化建议。",
  "tags": [
    "permissions",
    "least-privilege",
    "security",
    "audit"
  ]
}

path: resources/template.md
content: # 权限足迹审查员 输出模板

> 本模板由脚本和 Skill 共用。若无法自动执行，请按下面结构手工填写。

## 当前权限足迹
- 待填写：围绕“当前权限足迹”给出与 权限足迹审查员 场景相关的内容。

## 用途说明
- 待填写：围绕“用途说明”给出与 权限足迹审查员 场景相关的内容。

## 过度权限
- 待填写：围绕“过度权限”给出与 权限足迹审查员 场景相关的内容。

## 最小权限替代
- 待填写：围绕“最小权限替代”给出与 权限足迹审查员 场景相关的内容。

## 隔离建议
- 待填写：围绕“隔离建议”给出与 权限足迹审查员 场景相关的内容。

## 复核清单
- 待填写：围绕“复核清单”给出与 权限足迹审查员 场景相关的内容。

## 待确认项
- 如输入不足，请在这里明确列出缺失信息。

path: scripts/run.py
content: #!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter

BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"

def fail(message: str, code: int = 2) -> int:
    print(f"ERROR: {message}", file=sys.stderr)
    return code

def load_spec() -> dict:
    try:
        return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
    except FileNotFoundError:
        raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
    except json.JSONDecodeError as exc:
        raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))

def read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return path.read_text(encoding="utf-8", errors="replace")

def list_text_files(root: Path, limit: int = 50):
    results = []
    for path in root.rglob("*"):
        if len(results) >= limit:
            break
        if path.is_file():
            if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
                results.append(path)
    return results

def make_structured_report(spec: dict, input_text: str) -> str:
    title = spec["title"]
    summary = spec["summary"]
    sections = spec["sections"]
    bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
    bullets = bullets[:18]
    out = [f"# {title} 结果", "", f"> 模式：{spec['mode']}", f"> 摘要：{summary}", ""]
    for idx, section in enumerate(sections):
        out.append(f"## {section}")
        if bullets:
            selected = bullets[idx::max(1, len(sections))][:3]
            for item in selected:
                out.append(f"- {item}")
        else:
            out.append("- 输入材料不足，请补充更具体的原始信息。")
        out.append("")
    out.append("## 待确认项")
    out.append(f"- 请补充：{spec.get('inputHint', '更完整的输入材料')}")
    out.append("")
    out.append("## 下一步")
    out.append("- 先审阅上述结构，再决定是否进入执行、发送、发布或系统变更。")
    return "\n".join(out).strip() + "\n"

def directory_report(spec: dict, root: Path, limit: int) -> str:
    files = list_text_files(root, limit=limit)
    ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
    headings = []
    for p in files[: min(10, len(files))]:
        if p.suffix.lower() == ".md":
            text = read_text(p)
            for line in text.splitlines():
                if line.startswith("#"):
                    headings.append((p.name, line.strip()))
                    if len(headings) >= 12:
                        break
        if len(headings) >= 12:
            break
    out = [f"# {spec['title']} 扫描报告", "", f"扫描目录：`{root}`", f"文本文件样本数：{len(files)}", ""]
    out.append("## 目录概览")
    for p in files[:15]:
        out.append(f"- {p.relative_to(root)}")
    out.append("")
    out.append("## 扩展名分布")
    for ext, cnt in ext_counter.most_common():
        out.append(f"- {ext}: {cnt}")
    out.append("")
    out.append("## 标题样本")
    if headings:
        for fname, heading in headings:
            out.append(f"- {fname}: {heading}")
    else:
        out.append("- 未发现 Markdown 标题。")
    out.append("")
    for section in spec["sections"]:
        out.append(f"## {section}")
        out.append(f"- 基于目录和文件样本，围绕“{section}”给出人工审阅意见。")
        out.append("")
    return "\n".join(out).strip() + "\n"

def csv_report(spec: dict, path: Path, limit: int) -> str:
    delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
    rows = []
    with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
        reader = csv.DictReader(fh, delimiter=delimiter)
        for idx, row in enumerate(reader):
            rows.append(row)
            if idx + 1 >= limit:
                break
    if not rows:
        return make_structured_report(spec, "未读取到数据行。")
    fieldnames = list(rows[0].keys())
    out = [f"# {spec['title']} 数据报告", "", f"文件：`{path}`", f"采样行数：{len(rows)}", ""]
    out.append("## 字段概览")
    for field in fieldnames:
        values = [r.get(field, "") for r in rows]
        non_empty = [v for v in values if str(v).strip()]
        unique = len(set(non_empty))
        out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)}，唯一值约 {unique}")
    out.append("")
    for section in spec["sections"]:
        out.append(f"## {section}")
        out.append(f"- 结合字段概览与样本，围绕“{section}”补充判断。")
        out.append("")
    return "\n".join(out).strip() + "\n"

PATTERNS = {
    "curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
    "dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
    "base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
    "secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
    "private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}

def pattern_report(spec: dict, path: Path, limit: int) -> str:
    targets = [path] if path.is_file() else list_text_files(path, limit=limit)
    findings = []
    for target in targets:
        text = read_text(target)
        for name, pattern in PATTERNS.items():
            for match in re.finditer(pattern, text, flags=re.IGNORECASE):
                snippet = match.group(0)
                if "secret_like" == name:
                    snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
                findings.append((str(target), name, snippet[:160]))
                if len(findings) >= limit:
                    break
            if len(findings) >= limit:
                break
        if len(findings) >= limit:
            break
    out = [f"# {spec['title']} 模式扫描", "", f"扫描目标：`{path}`", ""]
    out.append("## 发现结果")
    if findings:
        for target, name, snippet in findings:
            out.append(f"- [{name}] {target}: `{snippet}`")
    else:
        out.append("- 未命中内置高风险模式。")
    out.append("")
    for section in spec["sections"]:
        out.append(f"## {section}")
        out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
        out.append("")
    return "\n".join(out).strip() + "\n"

def parse_frontmatter(path: Path):
    text = read_text(path)
    if not text.startswith("---\n"):
        return None, "SKILL.md 缺少前置 frontmatter"
    parts = text.split("\n---\n", 1)
    if len(parts) < 2:
        return None, "frontmatter 未正确闭合"
    front = parts[0].splitlines()[1:]
    data = {}
    for line in front:
        if not line.strip() or ":" not in line:
            continue
        key, value = line.split(":", 1)
        data[key.strip()] = value.strip()
    return data, None

def skill_audit(spec: dict, path: Path, limit: int) -> str:
    required = [
        "SKILL.md",
        "README.md",
        "SELF_CHECK.md",
        "scripts/run.py",
        "resources/spec.json",
        "resources/template.md",
        "examples/example-input.md",
        "tests/smoke-test.md",
    ]
    out = [f"# {spec['title']} 规范检查", "", f"检查目标：`{path}`", ""]
    out.append("## 文件完整性")
    for rel in required:
        target = path / rel
        out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
    out.append("")
    skill_md = path / "SKILL.md"
    if skill_md.exists():
        data, err = parse_frontmatter(skill_md)
        out.append("## Frontmatter")
        if err:
            out.append(f"- 错误：{err}")
        else:
            for key in ("name","description","version","metadata"):
                out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
            metadata_value = data.get("metadata", "")
            if metadata_value:
                try:
                    json.loads(metadata_value)
                    out.append("- metadata JSON: OK")
                except Exception as exc:
                    out.append(f"- metadata JSON: INVALID ({exc})")
    out.append("")
    for section in spec["sections"]:
        out.append(f"## {section}")
        out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
        out.append("")
    return "\n".join(out).strip() + "\n"

def build_report(spec: dict, source: Path, limit: int) -> str:
    mode = spec["mode"]
    if mode == "structured_brief":
        text = read_text(source) if source.exists() and source.is_file() else str(source)
        return make_structured_report(spec, text)
    if mode == "directory_audit":
        if not source.exists() or not source.is_dir():
            return make_structured_report(spec, f"目录不存在：{source}")
        return directory_report(spec, source, limit)
    if mode == "csv_audit":
        if not source.exists() or not source.is_file():
            return make_structured_report(spec, f"文件不存在：{source}")
        return csv_report(spec, source, limit)
    if mode == "pattern_audit":
        if not source.exists():
            return make_structured_report(spec, f"目标不存在：{source}")
        return pattern_report(spec, source, limit)
    if mode == "skill_audit":
        if not source.exists() or not source.is_dir():
            return make_structured_report(spec, f"Skill 目录不存在：{source}")
        return skill_audit(spec, source, limit)
    return make_structured_report(spec, f"未知模式：{mode}")

def main() -> int:
    parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
    parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
    parser.add_argument("--output", help="Write output to a file instead of stdout.")
    parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
    parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
    parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
    args = parser.parse_args()

    spec = load_spec()
    source = Path(args.input).expanduser()

    if source.exists():
        report = build_report(spec, source, args.limit)
    else:
        if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
            return fail(f"Input path does not exist: {source}")
        report = build_report(spec, Path(args.input), args.limit)

    if args.format == "json":
        payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
        rendered = json.dumps(payload, ensure_ascii=False, indent=2)
    else:
        rendered = report

    if args.dry_run or not args.output:
        print(rendered)
        return 0

    output_path = Path(args.output).expanduser()
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(rendered, encoding="utf-8")
    print(f"Wrote output to {output_path}")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())


path: tests/smoke-test.md
content: # 权限足迹审查员 冒烟测试

## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。

## 步骤
1. 检查目录包含必需文件：
   - `SKILL.md`
   - `README.md`
   - `SELF_CHECK.md`
   - `scripts/run.py`
   - `resources/spec.json`
   - `resources/template.md`
   - `examples/example-input.md`
2. 执行：
   ```bash
   python3 scripts/run.py --input examples/example-input.md --output out.md
   ```
3. 观察 `out.md` 是否成功生成，且至少包含以下章节：
   - 当前权限足迹
   - 用途说明
   - 复核清单
4. 执行异常路径：
   ```bash
   python3 scripts/run.py --input does-not-exist.md
   ```
5. 预期：
   - 正常路径返回 0 并生成结构化内容
   - 异常路径返回非 0，并输出可读错误信息

## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰


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