@yottameta/yotta-intel 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # 更新日志
2
2
 
3
+ ## v0.2.2 (2026-10-01)
4
+
5
+ - 安装器卫生批次:`bin/install.js` / `install.sh` 统一(未知参数报错 exit 2、`--help` / `--version`、残留清理白名单、嵌套载荷保留);由模板单一真源渲染,接入漂移门禁。
6
+ - 自带安装器测试(`test/install.test.js`)更新为统一行为断言。
7
+
8
+ ## v0.2.1 (2026-09-25)
9
+
10
+ 维护性发布:安装器加固 + README 锁定版本。
11
+
12
+ - 安装器:拒绝对符号链接目标写入、目标不是目录时拒绝、不做整目录删除。
13
+ - README(中英):安装命令改为锁定版本写法(去掉 `@版本` 即跟随最新)。
14
+ - 输出示例与 references 头部版本号同步 0.2.1。
15
+ - 修复:IPv4 前导零写法(`010.000.000.001`)归一为 `10.0.0.1` —— Python 3.9.5 起
16
+ `ipaddress.IPv4Address` 拒绝前导零八位组,旧引擎会静默丢掉该地址;`999.1.1.1` 仍判非法。
17
+
3
18
  ## v0.2.0 (2026-09-09)
4
19
 
5
20
  - 评测完善批 2:新增 references/faq.md(12 条常见问题 + 速查索引 + 安装排障);SKILL.md 增加 FAQ 速查节。
package/README.md CHANGED
@@ -91,7 +91,7 @@ python3 scripts/yotta_intel.py refang --path safe.txt
91
91
  Sample text output:
92
92
 
93
93
  ```
94
- 元情 yotta-intel v0.2.0 —— IOC 提取结果
94
+ 元情 yotta-intel v0.2.1 —— IOC 提取结果
95
95
  共发现 2 个 IOC:
96
96
 
97
97
  ■ IPv4 地址(ipv4)
@@ -108,8 +108,8 @@ Pick any of the four methods below; the order is the recommended priority. Skill
108
108
 
109
109
  ```text
110
110
  # Optional China mirror: npm config set registry https://registry.npmmirror.com
111
- npx -y @yottameta/yotta-intel --agent <agent-name> # install to the agent's default user-level skills dir
112
- npx -y @yottameta/yotta-intel --dir <your-skills-dir> # point to the skills dir itself (e.g. ~/.codex/skills)
111
+ npx -y @yottameta/yotta-intel@0.2.2 --agent <agent-name> # install to the agent's default user-level skills dir
112
+ npx -y @yottameta/yotta-intel@0.2.2 --dir <your-skills-dir> # point to the skills dir itself (e.g. ~/.codex/skills)
113
113
  ```
114
114
 
115
115
  - `--agent <name>` installs to that agent's default user-level directory; `--list` shows each agent's default directory.
package/README.zh-CN.md CHANGED
@@ -99,7 +99,7 @@ python3 scripts/yotta_intel.py refang --path safe.txt
99
99
  输出示例(text):
100
100
 
101
101
  ```
102
- 元情 yotta-intel v0.2.0 —— IOC 提取结果
102
+ 元情 yotta-intel v0.2.1 —— IOC 提取结果
103
103
  共发现 2 个 IOC:
104
104
 
105
105
  ■ IPv4 地址(ipv4)
@@ -116,8 +116,8 @@ python3 scripts/yotta_intel.py refang --path safe.txt
116
116
 
117
117
  ```text
118
118
  # 可选国内加速:npm config set registry https://registry.npmmirror.com
119
- npx -y @yottameta/yotta-intel --agent <智能体名称> # 装到指定智能体默认用户级技能目录
120
- npx -y @yottameta/yotta-intel --dir <智能体的技能目录> # 指到技能目录本身(如 ~/.codex/skills)
119
+ npx -y @yottameta/yotta-intel@0.2.2 --agent <智能体名称> # 装到指定智能体默认用户级技能目录
120
+ npx -y @yottameta/yotta-intel@0.2.2 --dir <智能体的技能目录> # 指到技能目录本身(如 ~/.codex/skills)
121
121
  ```
122
122
 
123
123
  - `--agent <name>` 自动装到该智能体默认用户级目录;`--list` 可查看各智能体默认目录。
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yotta-intel
3
- version: 0.2.0
3
+ version: 0.2.2
4
4
  description: 元情 —— 跨智能体的威胁情报 IOC 提取与规范化技能:零依赖自研从文本 / 日志 / 报告中提取 IP(IPv4/IPv6)、域名、URL、邮箱、哈希(MD5/SHA1/SHA256/SHA512)与 CVE 编号,识别并还原 defang 写法,去重、归一化后输出 CSV / JSON / STIX-lite。触发:用户给出含可疑 IP / 域名 / URL / 哈希的威胁情报文本、恶意样本分析报告、钓鱼邮件或日志,要提取 IOC、规范化、去重、转格式、共享情报时。边界:纯本地离线提取与规范化;不联网查证、不下载样本、不主动扫描任何系统;仅用于已获授权 / 自有资产 / 教学环境的安全分析。
5
5
  license: MIT
6
6
  ---
package/bin/install.js CHANGED
@@ -1,223 +1,314 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @yottameta/yotta-intel cross-platform installer.
3
+ * yotta-intel 跨平台安装器(YottaSkills)
4
4
  *
5
- * Usage:
6
- * npx -y @yottameta/yotta-intel --agent <name>
7
- * npx -y @yottameta/yotta-intel --dir <path>
8
- * npx -y @yottameta/yotta-intel -g
9
- * npx -y @yottameta/yotta-intel
10
- * npx -y @yottameta/yotta-intel --list
5
+ * 用法:
6
+ * npx -y @yottameta/yotta-intel --agent <name> # 按智能体默认用户级目录安装(推荐)
7
+ * npx -y @yottameta/yotta-intel --dir <path> # 装到指定目录(用户改了目录的智能体)
8
+ * npx -y @yottameta/yotta-intel -g # 安装到全部已知智能体用户级目录
9
+ * npx -y @yottameta/yotta-intel # 安装到检测到的项目级目录
10
+ * npx -y @yottameta/yotta-intel --list # 列出智能体 -> 默认目录
11
+ * npx -y @yottameta/yotta-intel --dry-run # 只显示将写入的目标,不写文件
12
+ * npx -y @yottameta/yotta-intel --version # 显示版本
13
+ * npx -y @yottameta/yotta-intel --help # 显示帮助
14
+ *
15
+ * 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
11
16
  */
12
17
  'use strict';
13
-
14
18
  const fs = require('fs');
15
19
  const path = require('path');
16
20
  const os = require('os');
17
21
 
18
22
  const SKILL_NAME = 'yotta-intel';
19
23
  const PKG_ROOT = path.join(__dirname, '..');
20
- const COPY_SKIP = new Set(['package.json', 'bin', 'node_modules', '.git', '.github', 'test']);
21
24
 
25
+ // 顶层开发文件 / 目录:只在安装包顶层跳过;技能包内嵌套同名载荷必须保留。
26
+ // 与 install.sh 同口径:开发件与运行时不相关目录一律不落目标。
27
+ const TOP_SKIP = [
28
+ 'package.json', 'package-lock.json', 'bin', 'lib', 'test',
29
+ '.github', '.git', '.gitignore', '.npmignore', '.gitattributes',
30
+ '.yotta', '.tmp', 'install.sh', 'node_modules',
31
+ ];
32
+ const TOP_SKIP_SET = new Set(TOP_SKIP);
33
+ // 缓存 / 编译产物:任意层级跳过,安装时同样清理。
34
+ const CACHE_DIRS = new Set(['__pycache__', '.pytest_cache', '.mypy_cache']);
35
+
36
+ class UsageError extends Error {}
37
+ class TargetError extends Error {}
38
+ class InstallError extends Error {}
39
+
40
+ // 智能体 -> 用户级默认技能目录(dirs 按优先级排列;--agent 装到第一个)
41
+ // 依据官方文档:.agents/skills 并非通用目录,被 OpenCode / Cursor / Cline / Amp /
42
+ // Kimi / Gemini CLI / GitHub Copilot 等读取;Claude Code 与 Codex 默认不读 .agents。
22
43
  const AGENT_DIRS = {
23
44
  claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
24
45
  cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
25
- codex: { label: 'Codex', dirs: ['.codex/skills'] },
46
+ codex: { label: 'Codex', dirs: ['.codex/skills'] }, // 特判:$CODEX_HOME/skills
26
47
  gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
27
48
  goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
28
49
  amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
29
- opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] },
50
+ opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] }, // 特判:$XDG_CONFIG_HOME
30
51
  windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
31
52
  workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
32
53
  kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
33
54
  trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
34
- 'trae-cn': { label: 'Trae IDE', dirs: ['.trae-cn/skills'] },
55
+ 'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills'] },
35
56
  qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
36
- comate: { label: 'Comate', dirs: ['.comate/skills'] },
57
+ comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
37
58
  codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
38
59
  kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
39
- agents: { label: 'Generic AGENTS.md', dirs: ['.agents/skills'] },
60
+ agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
40
61
  };
41
62
 
42
- class UsageError extends Error {}
43
- class TargetError extends Error {}
44
- class InstallError extends Error {}
63
+ const PROJECT_DIRS = [
64
+ '.claude/skills',
65
+ '.cursor/skills',
66
+ '.codex/skills',
67
+ '.config/goose/skills',
68
+ '.config/agents/skills',
69
+ '.opencode/skills',
70
+ '.codeium/windsurf/skills',
71
+ '.workbuddy/skills',
72
+ '.kiro/skills',
73
+ '.traecli/skills',
74
+ '.gemini/skills',
75
+ '.trae-cn/skills',
76
+ '.qwen/skills',
77
+ '.comate/skills',
78
+ '.codebuddy/skills',
79
+ '.kimi/skills',
80
+ '.agents/skills',
81
+ ];
82
+
83
+ // Codex 用户级目录特判:优先 $CODEX_HOME/skills,否则 ~/.codex/skills
84
+ function codexUserDir() {
85
+ const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
86
+ return path.join(base, 'skills');
87
+ }
88
+
89
+ // OpenCode 用户级目录特判:优先 $XDG_CONFIG_HOME/opencode/skills,否则 ~/.config/opencode/skills
90
+ function opencodeUserDir() {
91
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
92
+ return path.join(base, 'opencode', 'skills');
93
+ }
94
+
95
+ function resolveUserDir(rel) {
96
+ if (rel === '.codex/skills') return codexUserDir();
97
+ if (rel === '.config/opencode/skills') return opencodeUserDir();
98
+ return path.join(os.homedir(), rel);
99
+ }
100
+
101
+ function displayDir(rel) {
102
+ if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
103
+ return '~/' + rel;
104
+ }
105
+
106
+ function skillVersion() {
107
+ try {
108
+ const data = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
109
+ return data.version || null;
110
+ } catch (_) {
111
+ return null;
112
+ }
113
+ }
45
114
 
46
115
  function usage() {
47
- console.log('yotta-intel installer');
116
+ console.log(SKILL_NAME + ' 安装器(YottaSkills)');
48
117
  console.log('');
49
- console.log('Usage:');
50
- console.log(' npx -y @yottameta/yotta-intel --agent <name> Install to an agent default directory');
51
- console.log(' npx -y @yottameta/yotta-intel --dir <path> Install to a custom directory');
52
- console.log(' npx -y @yottameta/yotta-intel -g Install to all known agent directories');
53
- console.log(' npx -y @yottameta/yotta-intel Auto-detect project-level directories');
54
- console.log(' npx -y @yottameta/yotta-intel --list List supported agent directories');
118
+ console.log('用法:');
119
+ console.log(' node bin/install.js --agent <name> 按智能体默认用户级目录安装(推荐)');
120
+ console.log(' node bin/install.js --dir <path> 装到指定目录(用户改了目录的智能体)');
121
+ console.log(' node bin/install.js -g 安装到全部已知智能体用户级目录');
122
+ console.log(' node bin/install.js 安装到检测到的项目级目录');
55
123
  console.log('');
56
- console.log('Options:');
57
- console.log(' --agent <name> Agent key, see --list');
58
- console.log(' --dir <path> Custom skills directory');
59
- console.log(' -g, --global Install to all known user-level directories');
60
- console.log(' --list, -l List supported agents');
61
- console.log(' -h, --help Show this help');
124
+ console.log('参数:');
125
+ console.log(' --agent <name> 智能体键名,见 --list');
126
+ console.log(' --dir <path> 自定义技能目录');
127
+ console.log(' -g, --global 安装到全部已知用户级目录');
128
+ console.log(' --list, -l 列出支持的智能体目录');
129
+ console.log(' --dry-run 只显示将写入的目标,不写文件');
130
+ console.log(' --version, -v 显示版本');
131
+ console.log(' --help, -h 显示帮助');
132
+ console.log(' --yes, -y 兼容参数(-g 不再强制要求)');
133
+ console.log('');
134
+ console.log('退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误');
62
135
  }
63
136
 
64
137
  function parseArgs(argv) {
65
- const opts = { help: false, list: false, global: false, dir: null, agent: null };
138
+ const opts = { help: false, version: false, list: false, global: false, dryRun: false, yes: false, dir: null, agent: null };
66
139
  for (let i = 0; i < argv.length; i++) {
67
140
  const arg = argv[i];
68
141
  if (arg === '--help' || arg === '-h') opts.help = true;
142
+ else if (arg === '--version' || arg === '-v') opts.version = true;
69
143
  else if (arg === '--list' || arg === '-l') opts.list = true;
70
144
  else if (arg === '--global' || arg === '-g') opts.global = true;
145
+ else if (arg === '--dry-run') opts.dryRun = true;
146
+ else if (arg === '--yes' || arg === '-y') opts.yes = true;
71
147
  else if (arg === '--dir') {
72
148
  const value = argv[++i];
73
- if (!value) throw new UsageError('--dir requires a non-empty path');
149
+ if (!value) throw new UsageError('--dir 需要一个非空路径');
74
150
  opts.dir = value;
75
151
  } else if (arg === '--agent') {
76
152
  const value = argv[++i];
77
- if (!value) throw new UsageError('--agent requires a non-empty name');
78
- opts.agent = value.toLowerCase();
153
+ if (!value) throw new UsageError('--agent 需要一个非空名称');
154
+ opts.agent = String(value).toLowerCase();
79
155
  } else {
80
- throw new UsageError('Unknown argument: ' + arg);
156
+ throw new UsageError('未知参数: ' + arg);
81
157
  }
82
158
  }
83
- if (!opts.help) {
159
+ if (!opts.help && !opts.version) {
84
160
  const selected = [opts.dir, opts.agent, opts.global].filter(Boolean).length;
85
- if (selected > 1) throw new UsageError('Use only one of --dir, --agent, or -g');
161
+ if (selected > 1) throw new UsageError('--dir / --agent / -g 只能选一个');
86
162
  }
87
163
  return opts;
88
164
  }
89
165
 
90
- function codexUserDir() {
91
- const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
92
- return path.join(base, 'skills');
93
- }
94
-
95
- function opencodeUserDir() {
96
- const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
97
- return path.join(base, 'opencode', 'skills');
98
- }
99
-
100
- function resolveUserDir(rel) {
101
- if (rel === '.codex/skills') return codexUserDir();
102
- if (rel === '.config/opencode/skills') return opencodeUserDir();
103
- return path.join(os.homedir(), rel);
104
- }
105
-
106
- function displayDir(rel) {
107
- if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
108
- return '~/' + rel;
109
- }
110
-
111
166
  function printList() {
112
- console.log('Agent -> default skill directory:');
167
+ console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
113
168
  for (const [key, value] of Object.entries(AGENT_DIRS)) {
114
- console.log(' ' + key.padEnd(12) + value.label.padEnd(20) + value.dirs.map(displayDir).join(', '));
169
+ const resolved = value.dirs.map(displayDir);
170
+ console.log(' ' + key.padEnd(10) + value.label.padEnd(18) + resolved.join('、'));
115
171
  }
116
- console.log('');
117
- console.log('Use --dir <path> for agents not listed. CODEX_HOME and XDG_CONFIG_HOME are respected.');
172
+ console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
173
+ console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
118
174
  }
119
175
 
120
176
  function assertSafeTarget(target) {
121
177
  const rel = path.relative(PKG_ROOT, target);
122
178
  if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
123
- throw new UsageError('Target directory must be outside the skill source directory');
179
+ throw new UsageError('目标目录不能在技能源目录内(防止自装自毁)');
124
180
  }
125
181
  }
126
182
 
127
- function copyDir(src, dst, skip) {
183
+ function shouldSkipCache(name, isFile) {
184
+ if (CACHE_DIRS.has(name)) return true;
185
+ if (isFile && (name.endsWith('.pyc') || name.endsWith('.pyo'))) return true;
186
+ return false;
187
+ }
188
+
189
+ function copyDir(src, dst, topLevel) {
128
190
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
129
- if (skip.has(entry.name)) continue;
130
- const from = path.join(src, entry.name);
131
- const to = path.join(dst, entry.name);
132
- try {
191
+ // 只跳过安装包顶层开发文件;template/ 等嵌套同名载荷必须保留。
192
+ if (topLevel && TOP_SKIP_SET.has(entry.name)) continue;
193
+ if (shouldSkipCache(entry.name, entry.isFile())) continue;
194
+ const s = path.join(src, entry.name);
195
+ const d = path.join(dst, entry.name);
196
+ if (entry.isDirectory()) {
197
+ fs.mkdirSync(d, { recursive: true });
198
+ copyDir(s, d, false);
199
+ } else if (entry.isFile()) {
200
+ fs.copyFileSync(s, d);
201
+ }
202
+ }
203
+ }
204
+
205
+ // 清理旧版安装残留(fail-closed 白名单):
206
+ // 仅在目标目录已存在且含 SKILL.md 时触发;只删顶层开发项 + 任意层级缓存;
207
+ // 不整目录删除、不跟随符号链接。
208
+ function cleanResidue(target) {
209
+ const removed = [];
210
+ let stat = null;
211
+ try { stat = fs.statSync(target); } catch (_) { return removed; }
212
+ if (!stat.isDirectory()) return removed;
213
+ if (!fs.existsSync(path.join(target, 'SKILL.md'))) return removed;
214
+ for (const name of TOP_SKIP) {
215
+ const p = path.join(target, name);
216
+ let entry = null;
217
+ try { entry = fs.lstatSync(p); } catch (_) { continue; }
218
+ if (entry.isSymbolicLink()) continue;
219
+ fs.rmSync(p, { recursive: true, force: true });
220
+ removed.push(name);
221
+ }
222
+ const walk = (dir) => {
223
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
224
+ const p = path.join(dir, entry.name);
225
+ if (entry.isSymbolicLink()) continue;
133
226
  if (entry.isDirectory()) {
134
- fs.mkdirSync(to, { recursive: true });
135
- copyDir(from, to, skip);
136
- } else if (entry.isFile()) {
137
- fs.copyFileSync(from, to);
227
+ if (CACHE_DIRS.has(entry.name)) {
228
+ fs.rmSync(p, { recursive: true, force: true });
229
+ removed.push(path.relative(target, p));
230
+ } else {
231
+ walk(p);
232
+ }
233
+ } else if (entry.isFile() && (entry.name.endsWith('.pyc') || entry.name.endsWith('.pyo'))) {
234
+ fs.unlinkSync(p);
235
+ removed.push(path.relative(target, p));
138
236
  }
139
- } catch (err) {
140
- throw new InstallError('Failed to copy ' + from + ' -> ' + to + ': ' + err.message);
141
237
  }
142
- }
238
+ };
239
+ walk(target);
240
+ return removed;
143
241
  }
144
242
 
145
- function installTo(dest) {
146
- if (!dest || typeof dest !== 'string') throw new UsageError('Destination directory is required');
243
+ function installTo(dest, opts) {
244
+ if (!dest || typeof dest !== 'string') throw new UsageError('目标目录不能为空');
147
245
  const target = path.resolve(dest, SKILL_NAME);
148
246
  assertSafeTarget(target);
247
+ if (opts.dryRun) {
248
+ console.log('[dry-run] 将安装到 -> ' + target);
249
+ return target;
250
+ }
149
251
  try {
252
+ for (const rel of cleanResidue(target)) console.log('已清理残留: ' + rel);
150
253
  fs.mkdirSync(target, { recursive: true });
151
- copyDir(PKG_ROOT, target, COPY_SKIP);
254
+ copyDir(PKG_ROOT, target, true);
152
255
  if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
153
- throw new InstallError('Installed directory is missing SKILL.md');
256
+ throw new InstallError('安装结果缺少 SKILL.md');
154
257
  }
155
258
  } catch (err) {
156
- if (err instanceof UsageError || err instanceof InstallError) throw err;
157
- throw new InstallError('Cannot install to ' + target + ': ' + err.message);
259
+ if (err instanceof UsageError || err instanceof TargetError || err instanceof InstallError) throw err;
260
+ throw new InstallError('安装到 ' + target + ' 失败: ' + err.message);
158
261
  }
159
262
  console.log('installed -> ' + target);
160
263
  return target;
161
264
  }
162
265
 
163
- function projectDirs() {
164
- return [
165
- '.claude/skills',
166
- '.cursor/skills',
167
- '.codex/skills',
168
- '.config/goose/skills',
169
- '.config/agents/skills',
170
- '.opencode/skills',
171
- '.codeium/windsurf/skills',
172
- '.workbuddy/skills',
173
- '.kiro/skills',
174
- '.traecli/skills',
175
- '.gemini/skills',
176
- '.trae-cn/skills',
177
- '.qwen/skills',
178
- '.comate/skills',
179
- '.codebuddy/skills',
180
- '.kimi/skills',
181
- '.agents/skills',
182
- ].filter((dir) => fs.existsSync(dir));
183
- }
184
-
185
266
  function run() {
186
267
  const opts = parseArgs(process.argv.slice(2));
187
268
  if (opts.help) { usage(); return; }
269
+ if (opts.version) {
270
+ const version = skillVersion();
271
+ if (!version) {
272
+ console.error(SKILL_NAME + ' 版本未知(未找到 package.json)');
273
+ process.exitCode = 1;
274
+ return;
275
+ }
276
+ console.log(SKILL_NAME + ' v' + version);
277
+ return;
278
+ }
188
279
  if (opts.list) { printList(); return; }
189
280
 
190
- if (opts.dir) { installTo(opts.dir); return; }
281
+ if (opts.dir) { installTo(opts.dir, opts); return; }
191
282
 
192
283
  if (opts.agent) {
193
284
  const info = AGENT_DIRS[opts.agent];
194
285
  if (!info) {
195
- throw new UsageError('Unknown agent: ' + opts.agent + '. Available: ' + Object.keys(AGENT_DIRS).join(', ') + '. Use --dir for a custom directory.');
286
+ throw new UsageError('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';自定义目录请用 --dir <路径>。');
196
287
  }
197
- installTo(resolveUserDir(info.dirs[0]));
288
+ installTo(resolveUserDir(info.dirs[0]), opts);
289
+ console.log('完成。');
198
290
  return;
199
291
  }
200
292
 
201
293
  if (opts.global) {
202
294
  const seen = new Set();
203
- let count = 0;
204
295
  for (const value of Object.values(AGENT_DIRS)) {
205
- for (const dir of value.dirs) {
206
- if (seen.has(dir)) continue;
207
- seen.add(dir);
208
- installTo(resolveUserDir(dir));
209
- count++;
296
+ for (const rel of value.dirs) {
297
+ if (seen.has(rel)) continue;
298
+ seen.add(rel);
299
+ installTo(resolveUserDir(rel), opts);
210
300
  }
211
301
  }
212
- console.log('Installed to ' + count + ' directories.');
302
+ if (!opts.dryRun) console.log('完成。');
213
303
  return;
214
304
  }
215
305
 
216
- const dirs = projectDirs();
306
+ const dirs = PROJECT_DIRS.filter((d) => fs.existsSync(d));
217
307
  if (!dirs.length) {
218
- throw new TargetError('No project-level agent directory detected. Use --agent <name> or --dir <path>.');
308
+ throw new TargetError('未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。');
219
309
  }
220
- for (const dir of dirs) installTo(dir);
310
+ for (const dir of dirs) installTo(dir, opts);
311
+ if (!opts.dryRun) console.log('完成。');
221
312
  }
222
313
 
223
314
  function main() {
@@ -225,18 +316,18 @@ function main() {
225
316
  run();
226
317
  } catch (err) {
227
318
  if (err instanceof UsageError) {
228
- console.error('Usage error: ' + err.message);
319
+ console.error('用法错误: ' + err.message);
229
320
  usage();
230
321
  process.exitCode = 2;
231
322
  } else if (err instanceof TargetError) {
232
- console.error('Target error: ' + err.message);
323
+ console.error('目标错误: ' + err.message);
233
324
  process.exitCode = 4;
234
325
  } else if (err instanceof InstallError) {
235
- console.error('Install failed: ' + err.message);
236
- console.error('Fix: check directory permissions and free space, then retry. Use --dir to choose another directory.');
326
+ console.error('安装失败: ' + err.message);
327
+ console.error('修复建议: 检查目录权限与磁盘空间后重试,或用 --dir 换一个目录。');
237
328
  process.exitCode = 1;
238
329
  } else {
239
- console.error('Unexpected error: ' + (err && err.message ? err.message : String(err)));
330
+ console.error('未知错误: ' + (err && err.message ? err.message : String(err)));
240
331
  process.exitCode = 1;
241
332
  }
242
333
  }
package/install.sh CHANGED
@@ -1,28 +1,30 @@
1
1
  #!/usr/bin/env bash
2
- # yotta-intel 多智能体安装脚本(YottaSkills 模板)
2
+ # yotta-intel 多智能体安装脚本(YottaSkills)
3
3
  # 用法:
4
4
  # bash install.sh --agent <name> # 按智能体默认用户级目录安装
5
5
  # bash install.sh --dir <path> # 装到指定目录(用户改过目录的智能体)
6
6
  # bash install.sh -g # 装到全部已知智能体用户级目录
7
- # bash install.sh # 检测并安装到已存在的项目级目录
7
+ # bash install.sh # 检测并安装到已存在的项目级目录
8
8
  # bash install.sh --list # 列出智能体 -> 默认目录
9
+ # bash install.sh --dry-run # 只显示将写入的目标,不写文件
10
+ # bash install.sh --version # 显示版本
11
+ # bash install.sh --help # 显示帮助
12
+ # 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
9
13
  set -euo pipefail
10
14
 
11
15
  SKILL_NAME="yotta-intel"
12
16
  SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ _IS_WINDOWS=0
13
18
  case "$(uname -s)" in
14
19
  MINGW*|MSYS*)
15
20
  SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
21
+ _IS_WINDOWS=1
16
22
  ;;
23
+ CYGWIN*) _IS_WINDOWS=1 ;;
17
24
  esac
18
25
 
19
26
  # 智能体 -> 用户级默认目录(--agent 装到第一个)
20
27
  # .agents/skills 并非通用目录:OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot 读取。
21
- # 判断当前环境:Windows Git Bash 用 %USERPROFILE%,Unix 用 ~
22
- _IS_WINDOWS=0
23
- case "$(uname -s)" in
24
- MINGW*|MSYS*|CYGWIN*) _IS_WINDOWS=1 ;;
25
- esac
26
28
  dirs_for() {
27
29
  case "$1" in
28
30
  claude) echo ".claude/skills" ;;
@@ -60,17 +62,153 @@ resolve_user() {
60
62
  esac
61
63
  }
62
64
 
65
+ skill_version() {
66
+ local pkg="$SOURCE_DIR/package.json" v=""
67
+ [ -f "$pkg" ] || return 0
68
+ if command -v awk >/dev/null 2>&1; then
69
+ v="$(awk -F'"' '{for (i=1;i<=NF;i++) if ($i=="version") {print $(i+2); exit}}' "$pkg" 2>/dev/null || true)"
70
+ fi
71
+ if [ -z "$v" ]; then
72
+ v="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$pkg" 2>/dev/null | head -n 1 || true)"
73
+ fi
74
+ printf '%s' "$v"
75
+ }
76
+
77
+ usage() {
78
+ echo "$SKILL_NAME 安装脚本(YottaSkills)"
79
+ echo ""
80
+ echo "用法:"
81
+ echo " bash install.sh --agent <name> 按智能体默认用户级目录安装(推荐)"
82
+ echo " bash install.sh --dir <path> 装到指定目录(用户改了目录的智能体)"
83
+ echo " bash install.sh -g 安装到全部已知智能体用户级目录"
84
+ echo " bash install.sh 安装到检测到的项目级目录"
85
+ echo ""
86
+ echo "参数:"
87
+ echo " --agent <name> 智能体键名,见 --list"
88
+ echo " --dir <path> 自定义技能目录"
89
+ echo " -g, --global 安装到全部已知用户级目录"
90
+ echo " --list, -l 列出支持的智能体目录"
91
+ echo " --dry-run 只显示将写入的目标,不写文件"
92
+ echo " --version, -v 显示版本"
93
+ echo " --help, -h 显示帮助"
94
+ echo " --yes, -y 兼容参数(-g 不再强制要求)"
95
+ echo ""
96
+ echo "退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误"
97
+ }
98
+
99
+ resolve_path() {
100
+ # 与 SOURCE_DIR 同一风格(Windows Git Bash 用 pwd -W),供路径包含判断使用
101
+ local p="$1"
102
+ if [ "$_IS_WINDOWS" = "1" ]; then
103
+ (cd "$p" 2>/dev/null && pwd -W)
104
+ else
105
+ (cd "$p" 2>/dev/null && pwd -P)
106
+ fi
107
+ }
108
+
109
+ assert_safe_target() {
110
+ local dest="$1" src_real dest_real parent
111
+ src_real="$(resolve_path "$SOURCE_DIR")" || return 0
112
+ if [ -d "$dest" ]; then
113
+ dest_real="$(resolve_path "$dest")" || return 0
114
+ else
115
+ parent="$(dirname "$dest")"
116
+ dest_real="$(resolve_path "$parent")" || return 0
117
+ dest_real="$dest_real/$(basename "$dest")"
118
+ fi
119
+ case "$dest_real" in
120
+ "$src_real"|"$src_real"/*)
121
+ echo "安装失败:目标目录不能在技能源目录内(防止自装自毁)" >&2
122
+ return 1
123
+ ;;
124
+ esac
125
+ return 0
126
+ }
127
+
128
+ # 清理旧版安装残留(fail-closed 白名单):仅在目标目录已存在且含 SKILL.md 时触发;
129
+ # 只删顶层开发项 + 任意层级缓存;不整目录删除、不跟随符号链接。
130
+ DEV_SKIP="package.json package-lock.json bin lib test .github .git .gitignore .npmignore .gitattributes .yotta .tmp install.sh node_modules"
131
+ clean_residue() {
132
+ local dest="$1" name p
133
+ [ -d "$dest" ] || return 0
134
+ [ -f "$dest/SKILL.md" ] || return 0
135
+ for name in $DEV_SKIP; do
136
+ p="$dest/$name"
137
+ [ -L "$p" ] && continue
138
+ if [ -e "$p" ]; then
139
+ rm -rf -- "$p"
140
+ echo "已清理残留: $name"
141
+ fi
142
+ done
143
+ find "$dest" -type d \( -name '__pycache__' -o -name '.pytest_cache' -o -name '.mypy_cache' \) -prune -exec rm -rf -- {} + 2>/dev/null || true
144
+ find "$dest" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete 2>/dev/null || true
145
+ }
146
+
147
+ # 复制技能本体:顶层跳过开发件与运行时不相关目录(嵌套同名保留);
148
+ # 任意层级跳过缓存(__pycache__ / .pytest_cache / .mypy_cache / *.pyc / *.pyo)。
149
+ copy_tree() {
150
+ local src="$1" dst="$2" top="$3" entry base
151
+ mkdir -p "$dst"
152
+ for entry in "$src"/* "$src"/.[!.]* "$src"/..?*; do
153
+ [ -e "$entry" ] || [ -L "$entry" ] || continue
154
+ base="$(basename "$entry")"
155
+ case "$base" in
156
+ __pycache__|.pytest_cache|.mypy_cache) continue ;;
157
+ esac
158
+ if [ "$top" = "1" ]; then
159
+ case " $DEV_SKIP " in
160
+ *" $base "*) continue ;;
161
+ esac
162
+ fi
163
+ if [ -d "$entry" ] && [ ! -L "$entry" ]; then
164
+ copy_tree "$entry" "$dst/$base" 0 || return 1
165
+ else
166
+ case "$base" in
167
+ *.pyc|*.pyo) continue ;;
168
+ esac
169
+ cp -RP "$entry" "$dst/$base" || return 1
170
+ fi
171
+ done
172
+ }
173
+
63
174
  install_to() {
64
- mkdir -p "$1/$SKILL_NAME"
65
- cp -r "$SOURCE_DIR/." "$1/$SKILL_NAME/"
66
- rm -rf "$1/$SKILL_NAME/.git"
67
- echo "installed -> $1/$SKILL_NAME"
175
+ local base="$1" dry="$2" dest
176
+ dest="$base/$SKILL_NAME"
177
+ if [ -L "$base" ]; then
178
+ echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2
179
+ return 1
180
+ fi
181
+ if [ -e "$base" ] && [ ! -d "$base" ]; then
182
+ echo "安装失败:目标路径已存在且不是目录:$base" >&2
183
+ return 1
184
+ fi
185
+ if [ -L "$dest" ]; then
186
+ echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
187
+ return 1
188
+ fi
189
+ if [ -e "$dest" ] && [ ! -d "$dest" ]; then
190
+ echo "安装失败:技能路径已存在且不是目录:$dest" >&2
191
+ return 1
192
+ fi
193
+ assert_safe_target "$dest" || return 1
194
+ if [ "$dry" = "1" ]; then
195
+ echo "[dry-run] 将安装到 -> $dest"
196
+ return 0
197
+ fi
198
+ clean_residue "$dest"
199
+ mkdir -p "$dest" || return 1
200
+ copy_tree "$SOURCE_DIR" "$dest" 1 || return 1
201
+ if [ ! -f "$dest/SKILL.md" ]; then
202
+ echo "安装失败:结果缺少 SKILL.md" >&2
203
+ return 1
204
+ fi
205
+ echo "installed -> $dest"
68
206
  }
69
207
 
70
208
  list() {
71
209
  echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
210
+ local a dirs first
72
211
  for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
73
- local dirs first
74
212
  dirs="$(dirs_for "$a")"
75
213
  first="${dirs%% *}"
76
214
  case "$first" in
@@ -89,44 +227,101 @@ list() {
89
227
  }
90
228
 
91
229
  main() {
92
- local agent="" dir="" global=0 show_list=0
230
+ local agent="" dir="" global=0 show_list=0 show_help=0 show_version=0 dry=0
93
231
  while [ $# -gt 0 ]; do
94
232
  case "$1" in
95
- --agent) shift; agent="${1:-}" ;;
96
- --dir) shift; dir="${1:-}" ;;
97
- -g|--global) global=1 ;;
233
+ --help|-h) show_help=1 ;;
234
+ --version|-v) show_version=1 ;;
98
235
  --list|-l) show_list=1 ;;
236
+ -g|--global) global=1 ;;
237
+ --dry-run) dry=1 ;;
238
+ --yes|-y) : ;;
239
+ --agent)
240
+ shift
241
+ if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
242
+ echo "用法错误: --agent 需要一个非空名称" >&2
243
+ exit 2
244
+ fi
245
+ agent="$1"
246
+ ;;
247
+ --dir)
248
+ shift
249
+ if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
250
+ echo "用法错误: --dir 需要一个非空路径" >&2
251
+ exit 2
252
+ fi
253
+ dir="$1"
254
+ ;;
99
255
  *) echo "未知参数: $1" >&2; exit 2 ;;
100
256
  esac
101
257
  shift
102
258
  done
103
259
 
104
- if [ "$show_list" = "1" ]; then list; return; fi
105
- if [ -n "$dir" ]; then install_to "$dir"; echo "完成。"; return; fi
260
+ if [ "$show_help" = "1" ]; then usage; return 0; fi
261
+ if [ "$show_version" = "1" ]; then
262
+ local v
263
+ v="$(skill_version)"
264
+ if [ -z "$v" ]; then
265
+ echo "$SKILL_NAME 版本未知(未找到 package.json)" >&2
266
+ exit 1
267
+ fi
268
+ echo "$SKILL_NAME v$v"
269
+ return 0
270
+ fi
271
+ if [ "$show_list" = "1" ]; then list; return 0; fi
272
+
273
+ local selected=0
274
+ [ -n "$dir" ] && selected=$((selected + 1))
275
+ [ -n "$agent" ] && selected=$((selected + 1))
276
+ [ "$global" = "1" ] && selected=$((selected + 1))
277
+ if [ "$selected" -gt 1 ]; then
278
+ echo "用法错误: --dir / --agent / -g 只能选一个" >&2
279
+ exit 2
280
+ fi
281
+
282
+ if [ -n "$dir" ]; then
283
+ install_to "$dir" "$dry" || exit 1
284
+ [ "$dry" = "1" ] || echo "完成。"
285
+ return 0
286
+ fi
106
287
  if [ -n "$agent" ]; then
107
288
  local dirs first
108
289
  if ! dirs="$(dirs_for "$agent")"; then
109
- echo "未收录智能体: $agent。请用 --dir <路径> 指定技能目录。" >&2; exit 2
290
+ echo "未收录智能体: $agent。可用 --list 查看,或 --dir <路径> 指定。" >&2
291
+ exit 2
110
292
  fi
111
293
  first="${dirs%% *}"
112
- install_to "$(resolve_user "$first")"; echo "完成。"; return
294
+ install_to "$(resolve_user "$first")" "$dry" || exit 1
295
+ [ "$dry" = "1" ] || echo "完成。"
296
+ return 0
113
297
  fi
114
298
  if [ "$global" = "1" ]; then
115
- echo "安装到全部已知智能体用户级目录..."
116
- local dirs rel
299
+ local a dirs rel seen=" "
117
300
  for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
118
- dirs="$(dirs_for "$a")"
119
- for rel in $dirs; do install_to "$(resolve_user "$rel")"; done
301
+ dirs="$(dirs_for "$a")" || continue
302
+ for rel in $dirs; do
303
+ case "$seen" in
304
+ *" $rel "*) continue ;;
305
+ esac
306
+ seen="$seen$rel "
307
+ install_to "$(resolve_user "$rel")" "$dry" || exit 1
308
+ done
120
309
  done
121
- echo "完成。"; return
310
+ [ "$dry" = "1" ] || echo "完成。"
311
+ return 0
122
312
  fi
123
313
  local installed=0 d
124
314
  for d in .claude/skills .cursor/skills .codex/skills .config/goose/skills .config/agents/skills .opencode/skills .codeium/windsurf/skills .workbuddy/skills .kiro/skills .traecli/skills .gemini/skills .trae-cn/skills .qwen/skills .comate/skills .codebuddy/skills .kimi/skills .agents/skills; do
125
- if [ -d "$d" ]; then install_to "$d"; installed=1; fi
315
+ if [ -d "$d" ]; then
316
+ install_to "$d" "$dry" || exit 1
317
+ installed=1
318
+ fi
126
319
  done
127
320
  if [ "$installed" = "0" ]; then
128
- echo "未检测到项目级智能体目录。可用 --agent <name> / -g 装到用户级,或 --dir 指定。"
321
+ echo "目标错误: 未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。" >&2
322
+ exit 4
129
323
  fi
324
+ [ "$dry" = "1" ] || echo "完成。"
130
325
  }
131
326
 
132
- main "$@"
327
+ main "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-intel",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Yuanqing — a zero-dependency threat-intel IOC extraction & normalization engine for AI agents: extracts IP (IPv4/IPv6), domains, URLs, emails, hashes (MD5/SHA1/SHA256/SHA512) and CVE IDs from threat reports, phishing emails and logs; recognizes and reverses defanged forms (hxxp / [.] / (.) / [dot] / [:] / [@]); deduplicates, normalizes (IDN punycode, default-port stripping) and outputs CSV / JSON / STIX-lite (STIX 2.1 Bundle) with the pure Python 3.8+ standard library. Triggers when the user provides text containing suspicious IPs / domains / URLs / hashes and needs IOC extraction, normalization, dedup, format conversion, or safe sharing. Boundaries: fully local and offline; no reputation lookups, no sample downloads, no proactive scanning; authorized / own-asset / educational use only.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  # defang / refang 规则与共享建议
2
2
 
3
- > 元情 yotta-intel v0.2.0:去活性(defang)写法识别、还原与安全共享指引。
3
+ > 元情 yotta-intel v0.2.1:去活性(defang)写法识别、还原与安全共享指引。
4
4
 
5
5
  ## 1. 为什么要 defang
6
6
 
@@ -1,6 +1,6 @@
1
1
  # IOC 类型与判定规则(ioc-spec)
2
2
 
3
- > 元情 yotta-intel v0.2.0 内部规范:七类 IOC 的提取 / 归一 / 误报控制规则。
3
+ > 元情 yotta-intel v0.2.1 内部规范:七类 IOC 的提取 / 归一 / 误报控制规则。
4
4
  > 本文件是引擎行为的权威说明;SKILL.md 只放入口,细节在这里。
5
5
 
6
6
  ## 1. 支持的类型
@@ -1,6 +1,6 @@
1
1
  # STIX-lite 输出规范
2
2
 
3
- > 元情 yotta-intel v0.2.0:`extract --format stix` 的产物说明。
3
+ > 元情 yotta-intel v0.2.1:`extract --format stix` 的产物说明。
4
4
  > 「lite」= 只生成 STIX 2.1 的 Indicator 对象 + 自定义扩展属性,不实现完整 STIX 库。
5
5
 
6
6
  ## 1. 为什么叫 lite
@@ -488,7 +488,7 @@ class TestCLI(unittest.TestCase):
488
488
  def test_cli_version(self):
489
489
  r = run_cli("--version")
490
490
  self.assertEqual(r.returncode, 0)
491
- self.assertIn("0.2.0", r.stdout)
491
+ self.assertIn("0.2.2", r.stdout)
492
492
 
493
493
  def test_cli_unknown_type_exit4(self):
494
494
  r = run_cli("extract", "--stdin", "--types", "bogus", stdin="1.2.3.4")
@@ -52,7 +52,7 @@ try:
52
52
  except Exception:
53
53
  pass
54
54
 
55
- VERSION = "0.2.0"
55
+ VERSION = "0.2.2"
56
56
  TOOL = "yotta-intel"
57
57
  TOOL_CN = "元情"
58
58
 
@@ -188,6 +188,23 @@ URL_RE = re.compile(r"(?i)(?:(?:https?|ftp)://)[^\s<>\"',。!?;:、(
188
188
  EMAIL_RE = re.compile(r"(?i)[a-z0-9._%+\-]+@[a-z0-9\-]+(?:\.[a-z0-9\-]+)+")
189
189
  DOMAIN_RE = re.compile(r"(?<![\w@.])(?:[\w\-]{1,63}\.)+[\w\-]{2,63}(?![\-\w])")
190
190
  IPV4_RE = re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)")
191
+
192
+
193
+ def normalize_ipv4_octets(raw):
194
+ """把前导零写法(010.000.000.001)归一为 10.0.0.1。
195
+
196
+ Python 3.9.5 起 ipaddress.IPv4Address 拒绝前导零八位组,旧版本接受;
197
+ 这里先做十进制归一,保证跨版本行为一致(999.1.1.1 仍会被判非法)。
198
+ """
199
+ parts = raw.split(".")
200
+ if len(parts) != 4:
201
+ return raw
202
+ out = []
203
+ for part in parts:
204
+ if not part.isdigit() or len(part) > 3:
205
+ return raw
206
+ out.append(str(int(part)))
207
+ return ".".join(out)
191
208
  IPV6_TOKEN_RE = re.compile(r"(?i)(?<![\w:])(?:[0-9a-f:.]{2,45})(?![\w:])")
192
209
  HASH_RE = re.compile(r"(?i)(?<![0-9a-f])[0-9a-f]{32,128}(?![0-9a-f])")
193
210
  CVE_RE = re.compile(r"(?i)\bcve-\d{4}-\d{4,7}\b")
@@ -287,7 +304,7 @@ def find_iocs_in_line(line):
287
304
  for m in IPV4_RE.finditer(line):
288
305
  raw = m.group(0)
289
306
  try:
290
- ip = ipaddress.IPv4Address(raw)
307
+ ip = ipaddress.IPv4Address(normalize_ipv4_octets(raw))
291
308
  except ValueError:
292
309
  continue
293
310
  found.append((m.start(), m.end(), "ipv4", str(ip)))