技術メモを残していきます
AURの大規模マルウェア事件、自分の環境は大丈夫だった話
2026年6月、Arch Linux の AUR (Arch User Repository) で大規模なマルウェア混入事件が発生したらしい。 Arch Linux ユーザーとして、自分の環境への影響を確認したメモ。 何が起きたか Phoronixの報道によれば、400以上のAURパッケージが今週の大規模マルウェアキャンペーンで侵害されたとのこと 参考: https://www.phoronix.com/news/Arch-Linux-AUR-400-Compromised 事件は Arch Linux の公式メーリングリスト aur-general で報告・追跡されている 公式スレッド: https://lists.archlinux.org/archives/list/[email protected]/thread/FGXPCB3ZVCJIV7FX323SBAX2JHYB7ZS4/ 攻撃の手口 セキュリティベンダーSonatypeの分析(通称 “Atomic Arch”)によると、攻撃者はメンテナーが放棄した(orphaned)AURパッケージの所有権を奪い、ビルド手順(PKGBUILD)を改変して atomic-lockfile という悪意あるnpmパッケージをインストールさせる、というパターンが確認された。 参考: https://www.sonatype.com/blog/atomic-arch-npm-campaign-adds-malicious-dependency この atomic-lockfile は preinstall フックで Rust製のELFバイナリ(deps)を自動実行し、ブラウザやDiscord、GitHub、SSH、Dockerなど開発環境の認証情報を狙う情報窃取マルウェア 参考: https://ioctl.fail/preliminary-analysis-of-aur-malware/ 具体例として、VRストリーミング用パッケージ alvr に npm 関連の不自然な処理が追加されていたことが報告されている 参考: https://linuxiac.com/arch-linux-aur-malware-campaign-hits-multiple-user-contributed-packages/ 自分の環境を確認した paru -Qm でAUR(foreign)パッケージ一覧を出し、第三者がgit logから抽出した影響パッケージ一覧(gr.ht/aur_pkg_list.txt)と照合しました。手元の環境(google-chrome, postman-bin, ngrok など17パッケージ)はリストに一件も含まれてなかった ただしこのリストは非公式・暫定的なもので、対象期間も「直近48時間」程度のため、今後も対象が増える可能性がある。 今後の運用方針 緊急対応は不要と判断したが、完全に無視するのではい。 次回 paru -Syu 実行時に、各パッケージのPKGBUILD差分(特にbuild/installセクション)をざっと確認する binパッケージ(google-chrome, postman-binなど)はメンテナー変更の有無を paru -Si <pkg> で時々確認する というくらいのライトな運用はしようかなと思った。 今回のパターンは「放棄パッケージの乗っ取り」が中心なので、現役メンテナーがついているパッケージなら当面のリスクはかなり低いだろうがね。 所詮メモなので、注意喚起とかやりましょうとか強く推奨はしないがやったほうがいいとは思うかな。
svg-line: EmacsのステータスバーをSVGで統一する試み
元記事 svg-line: Better Status Bars for Emacs Emacsのmode-line、header-line、tab-bar、tab-lineはそれぞれネイティブAPIレベルで挙動が違い、多段表示・右寄せ・アイコン・マウスイベントの扱いがバラバラという問題がある。svg-lineはこれらをSVG画像として描画することで挙動を統一するパッケージ。 SVGにすることで座標ベースのマウスイベント検出ができるのも地味に大きい。ネイティブAPIだとクリックやホバーが*-lineごとに対応がバラバラだったのが一気に解決される。 この記事に触発されて私も少し触ってみようと思った。 あわよくばこのまま置き換えてもいいかとも思う。 やったこと 既存パッケージの整理 svg-lineを試すために一旦以下を削除・調整した。 nyan-mode → 削除 minions → 削除 spacious-padding → :mode-line-widthの行だけ削除(他の余白設定は残す) svg-lineの導入 (use-package svg-line :straight (:host github :repo "chiply/svg-line")) mode-lineを書いてみた 最小構成から始めて、バッファ名・git branch・メジャーモード・マイナーモード・行列数の2行構成を目指した。 (svg-line-define 'my-mode-line :target 'mode-line :active #'mode-line-window-selected-p :background (lambda () (face-background 'mode-line nil t)) :foreground (lambda () (face-foreground 'mode-line nil t)) :content (lambda () (list (list :left (list (if (buffer-modified-p) "● " " ") (buffer-name)) :right (list (or (and (fboundp 'vc-git--symbolic-ref) (buffer-file-name) (vc-git--symbolic-ref (buffer-file-name))) ""))) (cons (list (symbol-name major-mode)) (list (format-mode-line "%l:%c")))))) (svg-line-activate 'my-mode-line) シンプルにはなったが、やはりnyan-modeがないとさみしい。 ...
.zshrcのチューニング: 203msから79msへ
きっかけ Life is too short for a slow terminal を読んだ。 とりあえず「自分のzshも計測してみるか」となった。 流石にTerminalとかGPUパワーでFPS改善するとかフレームワーク使うのやめるだとか、ガッツリオリジナルコード書きまくるほどではないにしても明確にコレは駄目だというものがあれば改善したい。 先程の記事の筆者はoh-my-zshもpreztoも使わない主義で30msを達成しているが、私はp10kのUIを捨てるコストは払いたくなかったので、フレームワーク(zinit + p10k)は維持したまま改善できる部分だけ潰す方針にした。 まず計測 time zsh -i -c exit zsh -i -c exit 0.09s user 0.07s system 75% cpu 0.203 total 203ms。遅くはないが伸びしろがある気がする。 zprof で犯人を特定する .zshrc の先頭に: zmodload zsh/zprof 末尾に: zprof を追加して新しいシェルを開くと、関数ごとの所要時間テーブルが出る。上位30件だけ見れば十分。 zprof | head -n 30 num calls time self name ----------------------------------------------------------------------------------- 1) 1518 209.95 0.14 15.98% 153.70 0.10 11.70% :zinit-tmp-subst-zle 2) 60 107.81 1.80 8.20% 87.84 1.46 6.68% _zsh_autosuggest_async_request 3) 4 146.30 36.57 11.13% 67.91 16.98 5.17% _zsh_autosuggest_bind_widgets 4) 796 78.39 0.10 5.96% 67.84 0.09 5.16% _zsh_autosuggest_bind_widget 5) 34 98.12 2.89 7.47% 63.19 1.86 4.81% -fast-highlight-process 6) 2407 59.89 0.02 4.56% 59.89 0.02 4.56% .zinit-add-report ... zinit-tmp-subst-zle が1518回呼ばれていて1位。zinit がZLEウィジェットを差し替えるオーバーヘッドで、これはフレームワーク起因なので直接は触れない。 ...
WireGuard 経由で UNEXT が見れない問題を解決した話
環境 ラズパイ(Linux)がルーター兼 WireGuard クライアント 配下のデバイス(タブレット等)は eth0 経由でラズパイを通してインターネットへ ラズパイは wlan0 で ISP ルーター(192.168.x.1)に接続 全トラフィックを WireGuard(wg0)経由で VPS に流す構成 VPS は国内 VPS サービス タブレット(192.168.x.x) └─ eth0 ─ ラズパイ(ルーター) ├─ wlan0 ─ ISPルーター ─ インターネット └─ wg0 ─ VPS(国内) ─ インターネット 症状 WireGuard 経由の WiFi で UNEXT が一切見れない YouTube・Amazon Prime Video は問題なし UNEXT の生配信は見れる、VOD だけ駄目 調査 tcpdump で通信を確認 タブレットが UNEXT に接続しようとしたタイミングで tcpdump を仕掛けた。 sudo tcpdump -i eth0 -n 'src <タブレットIP> or dst <タブレットIP>' 2>/dev/null UNEXT のサーバーへの SYN が2回送られているが SYN-ACK が返ってこないことを確認。接続確立できていない。 ...
WireGuard 経由で UNEXT が見れない問題を調査した話【調査編】
解決編はこちら → WireGuard 経由で UNEXT が見れない問題を解決した話 | 怠惰技術ブログ 概要 WireGuard 経由の WiFi で UNEXT の VOD だけ見れないという問題を調査した。症状・仮説・コマンド・結果の思考トレースを残しておく。 最初の症状整理 UNEXT の VOD が見れない(くるくるのままタイムアウト) 生配信は見れる YouTube・Prime Video は問題なし この時点での仮説: MTU の問題(大きいパケットが通らない) QUIC(UDP)の問題 DRM 認証の問題 VPS 側のブロック Step 1: MTU を疑う WireGuard はオーバーヘッドがあるので MTU が小さくなる。大きいパケットが詰まってないか確認。 ping -M do -s 1400 8.8.8.8 結果: From 192.168.x.254 icmp_seq=1 Frag needed and DF set (mtu = 1420) ping: sendmsg: Message too long 1400 バイトは通らない。 1300 バイトは通った。MTU の壁が確認できた。 TCPMSS clamping を確認 sudo iptables -t mangle -L FORWARD -n -v TCPMSS tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp flags:0x06/0x02 TCPMSS clamp to PMTU すでに設定済みだった。TCP はケアされている。 ...
EDINET API v2 で有価証券報告書を自動取得する(Node.js / TypeScript)
EDINET は金融庁が運営する電子開示システムで、上場企業が提出した有価証券報告書・四半期報告書などを無償で取得できる API を提供している。 個人開発の財務分析ツールを作るにあたって、この API を Node.js / TypeScript で叩いた際のポイントをまとめる。 エンドポイント概要 ベース URL:https://api.edinet-fsa.go.jp/api/v2 用途 エンドポイント 書類一覧 GET /documents.json?date=YYYY-MM-DD&type=2 PDF取得 GET /documents/{docID}?type=2 XBRL取得 GET /documents/{docID}?type=1(ZIP) API キーはクエリパラメータ Subscription-Key で渡す。EDINET のサイト からアカウント登録すると発行される。ハードコードせず process.env.EDINET_API_KEY から読むのはもはや最低限のマナーといえる。 レスポンスの型定義 まず API レスポンスに型をつける。docID(大文字)が EDINET 公式の表記: interface EdinetDocumentResponse { docID: string // EDINET が振る書類ID docTypeCode: string | null secCode: string | null // 証券コード。上場企業以外は null edinetCode: string filerName: string docDescription: string | null submitDateTime: string } クライアントクラスで型を明示しておくと、後続のフィルタリングや保存ロジックで補完が効いて安全になる。 書類一覧の取得とフィルタリング /documents.json は指定日に提出されたすべての書類を返す。有価証券報告書だけを絞り込むには docTypeCode を見る: 120:有価証券報告書 130:訂正有価証券報告書 140:四半期報告書 また secCode が null の書類は上場企業以外なのでスキップする。 const filteredResults = results.filter((r: EdinetDocumentResponse) => { const typeCode = (r.docTypeCode ?? '').replace(/['"]/g, '') return ( r.secCode != null && (typeCode === '120' || typeCode === '130' || typeCode === '140') ) }) docTypeCode に余分なクォートが混入することがある("120" のように入ってくる)ので replace で除去している。実際にハマった。 ...
Gemini API で財務書類を「怪しさ判定」する:スコア付き出力の設計
個人開発の EDINET 分析ツールでは、取得した有価証券報告書の PDF を Gemini に渡して「怪しさ判定」をさせている。 単なる要約ではなく、3段階のスコア(normal / caution / danger) を返させる設計にしたので、その仕組みをまとめる。 なぜスコアが必要か 毎日数十〜数百件の書類が提出される。全部読むのは無理なので、AI に「これは要注意」かどうかを仕分けさせたい。 スコアが danger の書類だけ Discord 通知を飛ばす、といった使い方ができる。 プロンプト設計 プロンプトの末尾に必ずスコアを出力させるよう指示する: 分析の最後に必ず以下の形式でスコアを出力してください: SCORE:normal # 特に問題なし SCORE:caution # 気になる点あり・要確認 SCORE:danger # 重大なリスクの可能性 Gemini はマークダウン形式で分析テキストを返した後、最終行に SCORE:danger のような文字列を出力する。 PDF を渡す方法 @google/generative-ai SDK では PDF を base64 で渡せる: const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' }) const result = await model.generateContent([ { inlineData: { mimeType: 'application/pdf', data: pdfBuffer.toString('base64'), }, }, { text: prompt }, ]) 最大 50MB まで渡せるが、大きすぎるとトークン消費が跳ね上がるので注意。 スコアのパース 正規表現で SCORE: 以降を抽出: ...
Hono + TypeScript でクリーンアーキテクチャもどきを個人開発に持ち込む
個人開発に「クリーンアーキテクチャ」は過剰では?という気持ちはある。 ただ実際にやってみたら、テストが書きやすい・外部APIの差し替えが楽という恩恵がちゃんとあった。 Hono + TypeScript (ESM) でどう組んだかをメモしておく。 ディレクトリ構成 backend/src/ ├── domain/ # エンティティ・リポジトリ Interface │ ├── entity/ │ └── repository/ ├── usecase/ # ビジネスロジック ├── infrastructure/ # DB・外部API の実装 │ ├── postgres/ │ ├── edinet/ │ └── gemini/ ├── api/ # Hono ルーター └── job/ # JobRunner 依存の方向 api / job ↓ usecase ← domain (Interface) ↓ infrastructure → domain (Interface を実装) usecase は domain の Interface にしか依存しない。 infrastructure が Interface を実装する。これだけ守れば十分。 ...
Node.js でバックグラウンドジョブを自前実装する:PostgreSQL でジョブ管理
BullMQ や外部キューサービスを使わずに、PostgreSQL + while(true) ループでバックグラウンドジョブを管理する仕組みを作った。 「外部依存を増やしたくない」「DB を見るだけでジョブの状態がわかるようにしたい」という理由から。 ⚠️ この実装の前提条件(割り切りポイント) この仕組みは 「予算を抑えたい個人開発」かつ「アプリ単一インスタンス(1プロセス)」 での運用を前提に、あえてシンプルに作っています。 以下のトレードオフを理解した上で使ってください。 1. 厳密な重複排除はしていない(レースコンディション) アプリ側で isAnyRunning をチェック → ジョブ作成という 2 ステップになっているため、ミリ秒単位で同時リクエストが来た場合はすり抜ける可能性があります。 厳密に防ぐなら、後述する DB 側の Partial Unique Index が必要です。 2. マルチインスタンス非対応 起動時にジョブを一括リセットしているため、コンテナを複数台並列で動かす(水平拡張する)場合は、他インスタンスで実行中のジョブを巻き添えにします。 複数台にするなら worker_id カラムを導入するか、一括リセットをやめてください。 「バグ」ではなく「この規模だからこその意図的な割り切り」です。BullMQ を検討する規模になったら移行サインと考えています。 jobs テーブルの設計 CREATE TABLE jobs ( id SERIAL PRIMARY KEY, type VARCHAR(50) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', log TEXT, started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); status は pending → running → done / error と遷移する。 log カラムにジョブの実行ログを蓄積するので、Web UI から確認できる。 ...
ReactのuseStateでDate.now()を使うとlintエラーになる話
何が起きたか useState の初期値で Date.now() を直接呼んでいたら、こんなエラーが出た。 error Error: Cannot call impure function during render `Date.now` is an impure function. コード的にはこういうやつ。 const [stockPriceFrom, setStockPriceFrom] = useState<string>( new Date(Date.now() - 90 * 86400000).toISOString().split('T')[0] ) なぜエラーになるか React のルールとして、レンダー中はコンポーネントが pure でなければならない。 Date.now() や Math.random() はレンダーのたびに異なる値を返す「impure な関数」なので、直接渡すと React(特に Strict Mode)に怒られる。 Strict Mode ではレンダーを意図的に 2 回実行するため、こういった副作用が顕在化しやすい。 解決策:lazy initialization useState に関数を渡すと、初回マウント時に一度だけ実行される。これが lazy initialization パターン。 const [stockPriceFrom, setStockPriceFrom] = useState<string>( () => new Date(Date.now() - 90 * 86400000).toISOString().split('T')[0] ) const [stockPriceTo, setStockPriceTo] = useState<string>( () => new Date(Date.now() - 84 * 86400000).toISOString().split('T')[0] ) () => で包むだけ。それだけ。 まとめ パターン 評価タイミング useState(Date.now()) レンダーごとに評価される useState(() => Date.now()) 初回マウント時のみ new Date() も同様なので、日付系の初期値を useState に渡すときはアロー関数で包む癖をつけておくと良い。 ...