Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions public/registry/index.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
{
"version": 1,
"updatedAt": "2026-08-08T22:50:43.262Z",
"updatedAt": "2026-08-09T10:05:51.719Z",
"plugins": {
"count": 24,
"updatedAt": "2026-08-08T22:50:43.262Z"
"updatedAt": "2026-08-09T10:05:51.719Z"
},
"themes": {
"count": 54,
"updatedAt": "2026-08-08T22:50:43.262Z"
"updatedAt": "2026-08-09T10:05:51.719Z"
},
"widgets": {
"count": 45,
"updatedAt": "2026-08-08T22:50:43.262Z"
"updatedAt": "2026-08-09T10:05:51.719Z"
},
"skills": {
"count": 29,
"updatedAt": "2026-08-08T22:50:43.262Z"
"updatedAt": "2026-08-09T10:05:51.719Z"
},
"queries": {
"count": 24,
"updatedAt": "2026-08-08T22:50:43.262Z"
"updatedAt": "2026-08-09T10:05:51.719Z"
}
}
10 changes: 5 additions & 5 deletions public/registry/widgets.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-08-08T22:50:43.262Z",
"updatedAt": "2026-08-09T10:05:51.719Z",
"widgets": [
{
"id": "atcoder",
Expand Down Expand Up @@ -398,7 +398,7 @@
{
"id": "shodan-instance-audit",
"name": "Shodan インスタンス監査",
"version": "1.0.1",
"version": "1.0.2",
"author": "@hitalin",
"description": "指定した Misskey インスタンスのホストを Shodan で監査。露出ポートを危険度別に色分けし、稼働サービス・TLS 失効判定・CVE (CVSS と実証/推定の別)・Shodan のスキャン日を表示する管理者向けの外部露出チェック。結果はキャッシュされ、取得は初回と手動更新のみ (1 回 1 クエリクレジット)。API キーは Secret Vault (クエリ認証 param=key) で注入し widget.is には書かない",
"icon": "ti-radar",
Expand All @@ -419,9 +419,9 @@
],
"sourceUrl": "https://store.notedeck.io/registry/widgets/shodan-instance-audit/widget.is",
"apiUrl": "https://store.notedeck.io/registry/widgets/shodan-instance-audit/api.json",
"sha512": "e1237fed82973eba93f7b18dd1dd9852157f41ad885ef1c8e6d1c8c7d2e032e213bb5d2d976fe1206450dcf86db8ad1183a6fe7e16e70a5e1967e47403af874c",
"createdAt": "2026-08-08T22:50:46.721Z",
"updatedAt": "2026-08-08T22:50:46.721Z",
"sha512": "0338a2246f21ffd2e8d52d54e54b8c4703336a3651c406f71826515de1f404e224e43708e11d04dac9851c181324fc9381397560a641707e0d43ca58487f0d55",
"createdAt": "2026-08-09T07:54:40+09:00",
"updatedAt": "2026-08-09T07:54:40+09:00",
"authorUrl": "https://github.com/hitalin",
"license": "MIT",
"iconUrl": "https://store.notedeck.io/registry/widgets/shodan-instance-audit/icon.svg"
Expand Down
2 changes: 1 addition & 1 deletion public/registry/widgets/shodan-instance-audit/api.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/registry/widgets/shodan-instance-audit/meta.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "shodan-instance-audit",
"name": "Shodan インスタンス監査",
"version": "1.0.1",
"version": "1.0.2",
"author": "@hitalin",
"authorUrl": "https://github.com/hitalin",
"description": "指定した Misskey インスタンスのホストを Shodan で監査。露出ポートを危険度別に色分けし、稼働サービス・TLS 失効判定・CVE (CVSS と実証/推定の別)・Shodan のスキャン日を表示する管理者向けの外部露出チェック。結果はキャッシュされ、取得は初回と手動更新のみ (1 回 1 クエリクレジット)。API キーは Secret Vault (クエリ認証 param=key) で注入し widget.is には書かない",
Expand Down
54 changes: 48 additions & 6 deletions public/registry/widgets/shodan-instance-audit/widget.is
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,29 @@ var fromCache = false
/// 200 のときだけ JSON を返す。パース不能なら null
@jsonOf(res) {
if (res.status != 200) { return null }
let d = Json:parse(res.body)
let b = res.body
let t = Core:type(b)
// Vault の実装によっては本文が既にパース済みで返る。その場合はそのまま使う
if (t == "obj" || t == "arr") { return b }
// Json:parse は非文字列を渡すと例外で落ちるので必ず型を見てから呼ぶ
if (t != "str") { return null }
let d = Json:parse(b)
if (Core:type(d) != "obj" && Core:type(d) != "arr") { return null }
d
}

/// 解釈できなかったときに「何が返ってきたのか」を短く要約する。
/// これが無いと利用者も作者も原因を切り分けられない。
/// 本文は外部由来なので必ずサニタイズしてから表示する。
@bodyBrief(res) {
let b = res.body
let t = Core:type(b)
if (t != "str") { return `本文の型が {t} でした` }
if (b.len == 0) { return "本文が空でした" }
let head = if (b.len > 60) { `{b.slice(0, 60)}…` } else { b }
`{b.len} 文字: {sanitizeText(head)}`
}

/// data[] の banner から {port, product, version} を抽出
@normalizeServices(banners) {
if (Core:type(banners) != "arr") { return [] }
Expand Down Expand Up @@ -344,7 +362,14 @@ var fromCache = false
render()
}

@fetchData() {
@fetchData() { runAudit(false) }

/// 簡易表示。Shodan の minify=true は大きいフィールドを切り詰めて返すので、
/// 応答が肥大して壊れるホストでも通ることがある。
/// バナー詳細が落ちるぶん稼働サービス / TLS は出ないが、ポートと CVE は残る。
@fetchMinified() { runAudit(true) }

@runAudit(minify) {
if (busy) { return null } // 連打防止。1 押し = 1 クレジット
let h = normHost(host)
let herr = hostError(h)
Expand Down Expand Up @@ -383,7 +408,7 @@ var fromCache = false
}

// 2. ホスト詳細 (クレジットを 1 消費)
let hres = vaultGet(`/shodan/host/{ip}`)
let hres = if (minify) { vaultGet(`/shodan/host/{ip}?minify=true`) } else { vaultGet(`/shodan/host/{ip}`) }
if (hres.status == 404) {
// Shodan 未インデックス = 公開スキャンに引っかかっていない。防御的にはむしろ良い。
finish({ notIndexed: true, host: h, ip: ip, credits: fetchCredits() })
Expand All @@ -408,7 +433,15 @@ var fromCache = false

let data = jsonOf(hres)
if (Core:type(data) != "obj") {
fail("ホスト情報を解釈できませんでした", "Shodan の応答が JSON ではありませんでした")
// 何が返ってきたのかを出す。応答が途中で切れている場合は
// 簡易表示 (minify) で通ることがあるので、その導線も出す。
busy = false
result = {
error: "ホスト情報を解釈できませんでした"
hint: `応答: {bodyBrief(hres)}`
canMinify: !minify
}
render()
return null
}

Expand All @@ -422,6 +455,7 @@ var fromCache = false
finish({
host: h
ip: ip
minified: minify
org: get(data, "org", "")
country: get(data, "country_name", "")
ports: ports
Expand Down Expand Up @@ -626,6 +660,9 @@ var fromCache = false
pushAll(body, certView(result.certTs))
body.push(divider())
pushAll(body, vulnsView(result.vulns))
if (get(result, "minified", false)) {
body.push(Ui:C:mfm({ text: `<center>$[fg.color={MUTED} 簡易表示 (バナー詳細を省略して取得)]</center>` }))
}
pushAll(body, freshnessView(result))
pushAll(body, cacheView())
pushAll(body, creditsView(result.credits))
Expand Down Expand Up @@ -680,9 +717,14 @@ var fromCache = false
children = [
Ui:C:mfm({ text: `<center>$[fg.color={CRIT} ⚠️ {sanitizeText(result.error)}]</center>` })
Ui:C:mfm({ text: `<center>$[fg.color={MUTED} {sanitizeText(result.hint)}]</center>` })
configFolder(true)
Ui:C:button({ text: "再試行", primary: true, onClick: @() { fetchData() } })
]
// 応答が壊れていたときだけ、簡易表示で取り直す導線を出す
if (get(result, "canMinify", false)) {
children.push(Ui:C:mfm({ text: `$[fg.color={MUTED} 応答が大きすぎて途中で切れている可能性があります。簡易表示なら通ることがあります (バナー詳細は省略)]` }))
children.push(Ui:C:button({ text: "簡易表示で取得 (1 クレジット)", primary: true, onClick: @() { fetchMinified() } }))
}
children.push(configFolder(true))
children.push(Ui:C:button({ text: "再試行 (1 クレジット)", onClick: @() { fetchData() } }))
} else {
children = auditView(result)
children.push(divider())
Expand Down
Loading