
Byted Security Skillsscanner
- 31 installs
- 411 repo stars
- Updated August 4, 2026
- bytedance/agentkit-samples
byted-security-skillsscanner is a Claude skill that scans other skills for security risks by uploading them to Volcengine's scanning service.
About
A skill that audits other skills for security risks by packaging a target skill directory and uploading it to a Volcengine scanning service. A developer uses it to periodically scan workspace skills against a security policy, or to check a skill they are developing. It returns a JSON list of findings that is rendered as a risk report by severity.
- Packages and uploads a skill directory to Volcengine to scan it for security risks
- Audits other skills in the workspace for suspicious network calls or sensitive operations
- Outputs a risk report by level (High/Medium/Low) with rule name and remediation
Byted Security Skillsscanner by the numbers
- 31 all-time installs (skills.sh)
- Ranked #1,482 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
byted-security-skillsscanner capabilities & compatibility
- Capabilities
- security audit
- Use cases
- security audit
What byted-security-skillsscanner says it does
审计和扫描技能的安全性。当用户希望对工作区中的其他技能进行安全扫描时使用。
必须确保目标技能包含 `SKILL.md` 文件,因为它是扫描的主要输入。
npx skills add https://github.com/bytedance/agentkit-samples --skill byted-security-skillsscannerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 31 |
|---|---|
| repo stars | ★ 411 |
| Last updated | August 4, 2026 |
| Repository | bytedance/agentkit-samples ↗ |
What it does
Package a target skill directory, upload it to the Volcengine scanner, and report security risks by severity.
Who is it for?
auditing workspace skills or your own skill for security risks before use
Skip if: scanning targets that have no SKILL.md file
When should I use this skill?
user asks to security-scan or audit a skill directory
What you get
The target skill is scanned and a risk report is produced by severity with per-finding remediation advice.
- security scan report listing High/Medium risks with rule, file, detail, and remediation
By the numbers
- 3 required environment variables
- 3 risk levels (High/Medium/Low)
Files
Volcengine技能安全扫描器 (Volcengine Skills Scanner)
通过将技能目录打包并上传火山引擎安全扫描服务进行扫描,审计工作区中的其他技能是否存在潜在的安全风险。
何时使用
- 审计:定期扫描所有技能以确保符合安全策略。
- 开发:在开发过程中检查自己的技能。
- 要求:必须确保目标技能包含
SKILL.md文件,因为它是扫描的主要输入。
用法
使用 scripts/scan.py 脚本执行扫描。必须使用绝对路径,不要使用~,因为运行目录不是 skill 目录。
脚本会自动打包目录(如果提供的是目录)并上传,始终输出包含扫描结果的 JSON 数组。解析此JSON并以易读的格式(中文)向用户展示结果(风险等级、详细信息、建议)。
扫描技能(目录或压缩包)
脚本通过环境变量读取配置(推荐)
python3 ~/.openclaw/workspace/skills/byted-security-skillsscanner/scripts/scan.py --name "bad_skills1" --path "/root/.openclaw/workspace/skills/bad_skills1"重要:
- 脚本路径必须是绝对路径
- 目标路径也必须是绝对路径
- 确保已设置必要的环境变量(
VOLCENGINE_ACCESS_KEY、VOLCENGINE_SECRET_KEY、VOLCENGINE_REGION)
报告格式
向用户展示结果时,必须使用以下格式(中文):
🛡️ 安全扫描报告:[SkillName]
扫描时间: [将 ScanEndTime 时间戳转换为可读日期格式] 整体状态: [✅ 通过 / ❌ 发现风险]
| 风险等级 | 规则名称 | 风险详情 |
|---|---|---|
| [High/Medium/Low] | [RuleName] | [RiskDetail] |
发现的风险列表: (仅列出 High 和 Medium 级别的风险)
1. [RuleName] (ID: [RuleID])
- 等级: [RiskLevel]
- 文件: [FileName]
- 详情: [RiskDetail]
- 建议: 请检查上述文件中的代码,移除可疑的网络请求或敏感操作。
---
环境变量配置
1. 获取火山引擎访问凭证:参考 用户指南 获取 AK/SK
2. 配置以下环境变量:
export VOLC_ACCESS_KEY="your-access-key"
export VOLC_SECRET_KEY="your-secret-key"
export VOLC_REGION="cn-north-1" # 可选,默认 cn-north-1
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Volcengine 技能安全扫描器
简介
通过将技能目录打包并上传火山引擎安全扫描服务进行扫描,审计工作区中的其他技能是否存在潜在的安全风险。
何时使用
- 审计:定期扫描所有技能以确保符合安全策略。
- 开发:在开发过程中检查自己的技能。
- 要求:必须确保目标技能包含
SKILL.md文件,因为它是扫描的主要输入。
配置方式
方式一:OpenClaw 全局配置文件(推荐)
推荐在 OpenClaw 的全局配置文件 openclaw.json 中进行配置,这样无需每次手动设置环境变量,且能更安全地管理凭证。
配置文件路径:
~/.openclaw/openclaw.json配置内容: 在 skills.entries 字段下添加以下内容:
{
"skills": {
"entries": {
"volcengine_skills_scanner": {
"enabled": true,
"env": {
"VOLC_ACCESSKEY": "",
"VOLC_SECRETKEY": ""
}
}
}
}
}方式二:OpenClaw UI 配置
在 OpenClaw UI 中添加配置: 1. 进入【配置】 - 【Skills】 2. 添加对应的 Entries 以及 env 变量
方式三:环境变量配置
如果需要临时使用,可以直接设置环境变量:
export VOLC_ACCESSKEY="your-access-key"
export VOLC_SECRETKEY="your-secret-key"
export VOLC_REGION="cn-north-1" # 可选,默认 cn-north-1使用方式
使用 scripts/scan.py 脚本执行扫描:
python3 scripts/scan.py --name "skill-name" --path "/path/to/skill"参数说明:
--name: 技能名称(用于标识扫描任务)--path: 技能目录或压缩包路径(支持 ~ 展开用户主目录)
示例:
python3 scripts/scan.py --name feishu --path ~/.openclaw/workspace/skills/feishu-docs输出格式
脚本会自动打包目录(如果提供的是目录)并上传,输出包含扫描结果的 JSON 数组。
环境变量说明
| 变量名 | 说明 | 是否必须 | 默认值 |
|---|---|---|---|
VOLC_ACCESSKEY | 火山引擎 Access Key | 是 | - |
VOLC_SECRETKEY | 火山引擎 Secret Key | 是 | - |
VOLC_REGION | 火山引擎区域 | 否 | cn-north-1 |
SCAN_BASE_URL | 自定义扫描服务地址 | 否 | - |
获取访问凭证
参考火山引擎官方文档获取 AK/SK: 用户指南 - 获取访问密钥
# Volcengine Skills Scanner skill dependencies
requests>=2.28,<3
volcengine>=1.0,<2
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import json
import sys
import time
import argparse
import requests
import zipfile
import tempfile
import shutil
import urllib.parse
from volcengine.auth.SignerV4 import SignerV4
from volcengine.base.Request import Request
# Configuration Defaults
DEFAULT_BASE_URL = "http://127.0.0.1:8181/api/top/mana-open/cn-north-1/2025-01-01"
DEFAULT_REGION = "cn-north-1"
DEFAULT_SERVICE = "aisec"
DEFAULT_HOST = "https://open.volcengineapi.com"
# Scan Status Enum (mapped from API)
SCAN_STATUS_WAITING = "scanning"
SCAN_STATUS_RUNNING = "scanning"
SCAN_STATUS_SUCCESS = "success"
SCAN_STATUS_FAILED = "fail"
SCAN_STATUS_TIMEOUT = "timeout"
class Credentials:
def __init__(self, ak, sk, service, region, session_token=""):
self.ak = ak
self.sk = sk
self.access_key = ak
self.secret_key = sk
self.service = service
self.region = region
self.session_token = session_token
class SkillScanner:
def __init__(
self,
ak: str,
sk: str,
region: str = DEFAULT_REGION,
host: str = DEFAULT_HOST,
service: str = DEFAULT_SERVICE,
base_url: str = "",
session_token: str = "",
user_id: str = "",
):
self.ak = ak
self.sk = sk
self.region = region
self.service = service
self.host = host.rstrip("/")
self.user_id = user_id
# Initialize credentials
self.credentials = Credentials(ak, sk, service, region, session_token)
# Build base path and URLs
if base_url:
self.base_url = base_url.rstrip("/")
self.upload_url = f"{self.base_url}/UploadAndScanSkill"
self.detail_url = f"{self.base_url}/GetSkillScanDetail"
else:
self.base_path = f"/api/top/{self.service}/{self.region}/2026-01-04"
self.upload_url = f"{self.host}{self.base_path}/CreateSkill?Action=CreateSkill&Version=2026-01-04"
self.detail_url = f"{self.host}{self.base_path}/GetSkillScanDetail?Action=GetSkillScanDetail&Version=2026-01-04"
# Constants
self.SCAN_STATUS_SUCCESS = SCAN_STATUS_SUCCESS
self.SCAN_STATUS_FAIL = SCAN_STATUS_FAILED
def _zip_directory(self, dir_path):
"""Zip a directory and return the path to the temporary zip file."""
try:
temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
temp_zip.close()
with zipfile.ZipFile(temp_zip.name, "w", zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(dir_path):
for file in files:
if file.startswith(".") or file.startswith("._"):
continue
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, dir_path)
if arcname.lower() == "skill.md":
arcname = "skill.md"
elif arcname.lower().endswith("/skill.md"):
arcname = arcname[: -len("/skill.md")] + "/skill.md"
while arcname.startswith("./") or arcname.startswith("/"):
arcname = arcname.lstrip("./")
zipf.write(file_path, arcname)
return temp_zip.name
except Exception as e:
print(f"[Error] 压缩目录失败: {e}", file=sys.stderr)
return None
def _extract_data(self, response_json):
"""Extract Data from nested Result.Data structure."""
if not isinstance(response_json, dict):
return None
# Handle { "Result": { "Data": { ... } } }
if "Result" in response_json and isinstance(response_json["Result"], dict):
result = response_json["Result"]
if "Data" in result:
return result["Data"]
# Handle direct { "Data": { ... } } (if possible)
if "Data" in response_json:
return response_json["Data"]
# Handle flat structure or other variations if needed
return response_json
def upload_and_scan(self, name: str, path: str, description: str = "Security scan"):
"""Upload skill zip file and start scan."""
temp_zip_path = None
file_to_upload = path
try:
# Expand ~ in path for user home directory
path = os.path.expanduser(path)
if not os.path.exists(path):
return {"error": f"路径未找到: {path}"}
# File preparation logic
is_zip = path.lower().endswith(".zip") and os.path.isfile(path)
if is_zip:
# Use existing zip file
file_to_upload = path
elif os.path.isdir(path):
# Zip directory
# print(f"[*] Zipping directory: {path}")
temp_zip_path = self._zip_directory(path)
if not temp_zip_path:
return {"error": "目录压缩失败"}
file_to_upload = temp_zip_path
else:
# File that is not a zip (could be other archive or single file)
# print(f"[*] Processing non-zip file: {path}")
# Check if it is a supported archive format
try:
# Attempt to unpack to a temporary directory
extract_temp_dir = tempfile.mkdtemp()
success_unpack = False
try:
# Try standard unpack_archive first (tar, gztar, zip, etc)
shutil.unpack_archive(path, extract_temp_dir)
success_unpack = True
except Exception:
pass
if not success_unpack:
# Fallback for plain .gz file
if path.lower().endswith(".gz"):
try:
shutil.unpack_archive(
path, extract_temp_dir, format="tar"
)
success_unpack = True
except Exception:
pass
if not success_unpack and not path.lower().endswith(
".tar.gz"
):
try:
import gzip
# Guess original filename: strip .gz
original_name = os.path.basename(path)[:-3]
output_file = os.path.join(
extract_temp_dir, original_name
)
with gzip.open(path, "rb") as f_in:
with open(output_file, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
success_unpack = True
except Exception as gz_err:
shutil.rmtree(extract_temp_dir)
raise ValueError(f"无法解压GZ文件: {gz_err}")
if not success_unpack:
shutil.rmtree(extract_temp_dir)
raise ValueError("不支持的归档格式或解压失败")
# If successful, re-zip the extracted content
temp_zip_path = self._zip_directory(extract_temp_dir)
shutil.rmtree(extract_temp_dir)
if not temp_zip_path:
return {"error": "重新压缩解压内容失败"}
file_to_upload = temp_zip_path
except ValueError as ve:
# Explicit validation error
return {"error": f"文件格式无效: {str(ve)}"}
except Exception as e:
# General error
return {
"error": f"处理文件失败 '{os.path.basename(path)}': {str(e)}"
}
# Prepare multipart upload
with open(file_to_upload, "rb") as f:
file_content = f.read()
files = {
"File": (
os.path.basename(file_to_upload),
file_content,
"application/zip",
)
}
data = [
("Name", name),
("Description", description),
("IntegrateType", "file"),
("ScanNow", "true"),
]
# Retry logic for upload
max_retries = 3
for attempt in range(max_retries):
try:
# Prepare the request to get headers/body for signing
req = requests.Request(
"POST", self.upload_url, files=files, data=data
)
prepped = req.prepare()
# Use SDK to sign
parsed_url = urllib.parse.urlparse(self.upload_url)
v_request = Request()
v_request.method = "POST"
v_request.host = parsed_url.netloc
v_request.path = parsed_url.path
v_request.query = dict(urllib.parse.parse_qsl(parsed_url.query))
v_request.headers = dict(prepped.headers)
v_request.headers["Host"] = parsed_url.netloc
v_request.body = prepped.body
SignerV4.sign(v_request, self.credentials)
if len(v_request.body) < 2000:
# Only show short bodies safely
print(f"Body Preview: {v_request.body}")
else:
print("Body Preview (too long): <multipart form>")
response = requests.post(
self.upload_url,
headers=v_request.headers,
data=prepped.body,
timeout=60,
)
try:
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if (
response.status_code in [401, 403, 429]
or response.status_code >= 500
):
if attempt < max_retries - 1:
time.sleep(1 * (attempt + 1)) # Backoff
continue
try:
error_detail = response.json()
except ValueError:
error_detail = response.text
return {"error": f"上传失败: {str(e)}", "details": error_detail}
except requests.exceptions.RequestException as e:
if attempt < max_retries - 1:
time.sleep(1 * (attempt + 1))
continue
return {"error": f"上传失败 (网络错误): {str(e)}"}
except Exception as e:
return {"error": f"上传失败 (未知错误): {str(e)}"}
return {"error": "上传失败: 超过最大重试次数"}
except Exception as e:
return {"error": f"上传失败: {str(e)}"}
finally:
if temp_zip_path and os.path.exists(temp_zip_path):
try:
os.unlink(temp_zip_path)
except OSError:
pass
def get_scan_detail(self, skill_id: str):
"""Poll for scan results."""
payload = json.dumps({"SkillID": skill_id})
# Max retries: 10 mins (20s * 30)
max_retries = 30
for i in range(max_retries):
try:
headers = {"Content-Type": "application/json"}
parsed_url = urllib.parse.urlparse(self.detail_url)
v_request = Request()
v_request.method = "POST"
v_request.host = parsed_url.netloc
v_request.path = parsed_url.path
v_request.query = dict(urllib.parse.parse_qsl(parsed_url.query))
v_request.headers = headers
v_request.headers["Host"] = parsed_url.netloc
v_request.body = (
payload.encode("utf-8") if isinstance(payload, str) else payload
)
SignerV4.sign(v_request, self.credentials)
response = requests.post(
self.detail_url, data=payload, headers=v_request.headers, timeout=60
)
response.raise_for_status()
result_json = response.json()
scan_data = self._extract_data(result_json)
if not scan_data:
time.sleep(5)
continue
status = scan_data.get("ScanStatus")
if status == self.SCAN_STATUS_SUCCESS:
return scan_data
elif status == self.SCAN_STATUS_FAIL:
return {
"error": f"扫描失败: {status}",
"details": scan_data.get("ScanErrMsg"),
}
# Waiting/Running
time.sleep(20)
except Exception:
time.sleep(5)
return {"error": "等待扫描结果超时"}
def main():
parser = argparse.ArgumentParser(
description="Scan OpenClaw skills via Mana Open API or Volcengine Public Cloud."
)
parser.add_argument("--name", required=True, help="Name of the skill")
parser.add_argument(
"--path", required=True, help="Path to the skill directory or zip file"
)
args = parser.parse_args()
# Read configuration from environment variables
base_url = os.environ.get("SCAN_BASE_URL", "")
ak = (
os.environ.get("VOLC_ACCESS_KEY")
or os.environ.get("VOLC_ACCESSKEY")
or os.environ.get("SCAN_AK")
)
sk = (
os.environ.get("VOLC_SECRET_KEY")
or os.environ.get("VOLC_SECRETKEY")
or os.environ.get("SCAN_SK")
)
region = os.environ.get("VOLC_REGION", DEFAULT_REGION)
host = os.environ.get("SCAN_SERVICE_HOST", DEFAULT_HOST)
service = os.environ.get("VOLC_SERVICE", DEFAULT_SERVICE)
if not ak or not sk:
if base_url and "127.0.0.1" not in base_url and "localhost" not in base_url:
print(
json.dumps(
[
{
"error": "Missing VOLC_ACCESS_KEY or VOLC_SECRET_KEY environment variables"
}
],
indent=2,
)
)
return
else:
ak = ak or "test_ak"
sk = sk or "test_sk"
base_url = base_url or DEFAULT_BASE_URL
# Initialize scanner with public cloud or local config
if base_url:
scanner = SkillScanner(ak, sk, region, base_url=base_url)
else:
scanner = SkillScanner(ak, sk, region, host, service)
# 1. Upload and start scan
upload_result = scanner.upload_and_scan(args.name, args.path)
if "error" in upload_result:
print(json.dumps([upload_result], indent=2))
return
# Extract SkillID
upload_data = scanner._extract_data(upload_result)
if not upload_data:
skill_id = upload_result.get("SkillID")
else:
skill_id = upload_data.get("SkillID")
if not skill_id:
print(
json.dumps(
[{"error": "No SkillID returned from upload", "raw": upload_result}],
indent=2,
)
)
return
# 2. Get scan details
scan_result = scanner.get_scan_detail(skill_id)
# Wrap in list for consistency
print(json.dumps([scan_result], indent=2))
if __name__ == "__main__":
main()
Related skills
FAQ
What does the scanner need as input?
A skill directory or zip; the target must contain a SKILL.md file, since it is the main scan input.
How are results shown?
As a Chinese report grouping High and Medium risks with rule name, file, detail, and remediation.