検証で実際に検出された(あるいは検出されなかった)ルールの解説です。
S106 のような ID だけ見せられても分からないので、何を問題としているのか・
どう直すのか・Vue3 / DRF ではどこに出るのかをまとめています。
環境: SonarQube Community Build 26.9.0.129388
1. ルール ID の読み方
python:S3776
└─┬──┘ └─┬─┘
│ └── ルール番号
└──────── リポジトリ(どのアナライザのルールか)
リポジトリの種類
| プレフィックス | 担当 | 今回出た例 |
|---|---|---|
python: |
Python アナライザ | python:S3776 |
typescript: |
TypeScript アナライザ | typescript:S106 |
javascript: |
JavaScript アナライザ | javascript:S106 |
secrets: |
シークレット検出(全ファイルをテキスト走査) | secrets:S6687 |
Web: |
HTML / テンプレート | Web:InputWithoutLabelCheck |
css: |
CSS / SCSS | — |
docker: kubernetes: terraform:
|
IaC | — |
.vue ファイルは typescript: または javascript: が担当します(<script setup lang="ts">
なら TypeScript)。テンプレート部分の一部は Web: が見ます。
重要:番号は言語をまたいで共通
同じ番号は、言語が違っても同じ概念を指します。
| 番号 | 概念 | Python | TypeScript |
|---|---|---|---|
S1135 |
TODO コメント | python:S1135 |
typescript:S1135 |
S2245 |
擬似乱数の使用 | python:S2245 |
typescript:S2245 |
S3776 |
認知的複雑度 | python:S3776 |
typescript:S3776 |
S1862 |
同一条件の分岐 | python:S1862 |
typescript:S1862 |
S125 |
コメントアウトされたコード | python:S125 |
typescript:S125 |
ただし有効/無効は言語ごとに別です。今回 S125 は Python 側だけ有効で、
TypeScript 側は無効でした。
Web:InputWithoutLabelCheck のような ID
古いルールは番号ではなく名前が付いています。S で始まらないものは
そういう歴史的な事情です。
2. 深刻度(Severity)は 2 系統が併存している
ここが紛らわしいところです。SonarQube には新旧 2 つの深刻度体系があり、
API では両方返ります。
| 体系 | 値 | |
|---|---|---|
| 旧(Severity) | ルールごとに固定 |
BLOCKER / CRITICAL / MAJOR / MINOR / INFO
|
| 新(Impact Severity) | Clean Code Taxonomy |
BLOCKER / HIGH / MEDIUM / LOW / INFO
|
画面は基本的に新体系で表示されますが、/api/issues/search の severity
フィールドは旧体系を返します。記事やレポートで数値を出すときは、
どちらの体系か明示した方が安全です。
実測(backend 修正前 14 件)の内訳:
旧: BLOCKER 1 / CRITICAL 4 / MAJOR 3 / MINOR 3 / INFO 1(+ 他)
新: BLOCKER 1 / HIGH 6 / MEDIUM 3 / LOW 3 / INFO 1
3. Bug(動作が壊れるもの)
python:S1763 / typescript:S1763 — 到達不能コード
return の後ろに書かれたコードは実行されません。
def legacy_health_check():
status = {"status": "ok"}
return status
status["checked"] = True # ← ここには絶対に到達しない
直し方: 削除する。もし実行したいなら return の前に移す。
実務での出方: return を早期リターンに書き換えたときの取り残し、
デバッグ用コードの置き忘れ。
python:S1862 / typescript:S1862 — 同じ条件の分岐
if / elif に同じ条件が 2 回あると、後ろの分岐は永久に実行されません。
if todo.priority == "high":
summary["high"] += 1
elif todo.priority == "mid":
summary["mid"] += 1
elif todo.priority == "high": # ← 1 つ目と同じ条件。到達しない
summary["warnings"].append("unknown priority")
} else if (todo.priority === 'low') {
label = '低'
} else if (todo.priority === 'high') { // ← 到達しない
label = '不明'
}
直し方: 条件を正しいものに直す。または辞書引き / in 判定に置き換える。
# 条件の重複が起きない書き方
if todo.priority in ("high", "mid", "low"):
summary[todo.priority] += 1
else:
summary["warnings"].append("unknown priority")
実務での出方: コピペで分岐を増やしたときの書き換え漏れ。 最も現実的なバグ。
typescript:S1656 — 自己代入
変数を自分自身に代入しても何も起きません。
function add(title: string): void {
title = title // ← 何の意味もない
todos.value.push({ title, ... })
}
直し方: 加工するつもりだったなら加工を書く。不要なら削除する。
title = title.trim() // 本来やりたかったこと
実務での出方: 「ここで整形しよう」と書き始めて忘れた跡。
python:S5863 — 同じ式を左右に置いた assert
比較の両辺が同じ式だと、テストとして意味がありません。
# ソルトが効いていることを確認したいが、これでは指摘される
assert hash_password("same") != hash_password("same")
直し方: 一度変数に受ける。
first = hash_password("same")
second = hash_password("same")
assert first != second
注意: 上の例は意図としては正しい(毎回違うハッシュになることの確認)のですが、
静的解析からは「同じ式の比較」に見えます。変数に分ければ意図も明確になります。
Web:InputWithoutLabelCheck — input に label がない
アクセシビリティのルールです。Bug に分類されます。
<!-- 指摘される -->
<input v-model="draft" placeholder="やることを入力" />
直し方: id を付けて label と関連付ける。
<label for="todo-draft">やること</label>
<input id="todo-draft" v-model="draft" />
Vue3 で頻出します。 placeholder だけで済ませているフォームは全部これが出ます。
4. Vulnerability(セキュリティ)
secrets:S6687 — Django の SECRET_KEY のハードコード
BLOCKER。今回最も重い指摘でした。
SECRET_KEY = "django-insecure-3k9v2m8x7q1w5e4r6t8y0u2i4o6p8a0s2d4f6g8h"
メッセージは Make sure this Django key gets revoked, changed, and removed from the code.
つまり 「無効化して、変更して、コードから消せ」。
直し方: 環境変数から読む。
from django.core.management.utils import get_random_secret_key
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") or get_random_secret_key()
重要: このルールは secrets: リポジトリなので、Python の構文解析ではなく
全ファイルのテキスト走査で見つかります。.env をコミットした場合も拾われます。
django-insecure- で始まる鍵は startproject が生成するものです。
そのまま本番に出ているプロジェクトは珍しくないので、導入直後に必ず出ます。
python:S4507 — デバッグ機能の有効化
DEBUG = True
Django の DEBUG = True は、例外時にソースコード・環境変数・SQL を
ブラウザに表示します。本番で有効だと情報漏洩に直結します。
直し方: 既定を False にし、有効化を明示的な指定に限る。
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"
python:S4502 — CSRF 保護の無効化
今回 2 箇所で出ました。両方 DRF 開発で踏みやすいパターンです。
① @csrf_exempt の使用
@csrf_exempt
def evaluate_filter(request):
...
② settings.MIDDLEWARE に CsrfViewMiddleware が無い
MIDDLEWARE = [
"django.middleware.common.CommonMiddleware",
# CsrfViewMiddleware が無い → ファイル全体に対して指摘される
]
直し方: ミドルウェアを追加する。
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
]
DRF 利用時の注意: DRF の APIView は内部で csrf_exempt されているため、
ミドルウェアを入れても API は動きます。トークン認証の API だから CSRF 不要、
という判断は正しいのですが、ミドルウェア自体は入れておくべきです
(Django の管理画面やセッション認証を併用する場合に効きます)。
python:S4790 — 脆弱なハッシュアルゴリズム
return hashlib.md5(raw_password.encode("utf-8")).hexdigest()
MD5 / SHA-1 は衝突が現実的に可能で、パスワードハッシュには使えません
(高速すぎて総当たりに弱い)。
直し方: Django なら標準のハッシャを使う。
from django.contrib.auth.hashers import make_password
return make_password(raw_password) # 既定は PBKDF2
注意: メッセージは Make sure that hashing data is safe here. と
「確認してください」という書き方です。チェックサム用途の MD5 は問題ないため、
機械的に危険と断定しない設計になっています。
python:S2245 / typescript:S2245 — 擬似乱数の使用
suffix = "".join(random.choice(alphabet) for _ in range(16))
return Math.random().toString(36).slice(2)
random / Math.random() は予測可能です。トークン・セッション ID・
パスワードリセットのキーに使うと推測されます。
直し方:
import secrets
return f"{todo_id}-{secrets.token_urlsafe(12)}"
// ブラウザ
crypto.randomUUID()
crypto.getRandomValues(new Uint8Array(16))
Vue3 でありがちなのは、一時的な key 生成に Math.random() を使うケースです。
表示用の key なら実害はないので、その場合は False Positive ではなく
「Won't Fix / Accept」で処理するのが筋です。
python:S4830 — TLS 証明書検証の無効化
requests.post(url, json=payload, verify=False) # ← 中間者攻撃を受ける
直し方: verify を指定しない(既定で有効)。社内の自己署名証明書が理由なら、
CA 証明書を指定する。
requests.post(url, json=payload, verify="/path/to/internal-ca.pem")
実務での出方: 「開発環境で証明書エラーが出るから」と verify=False を入れて
そのまま本番に出る、という典型パターン。
python:S5443 — 誰でも書けるディレクトリの使用
log_path = os.environ.get("TODO_ARCHIVE_LOG", "/tmp/todo-archive.log")
/tmp は全ユーザーが書けるため、シンボリックリンク攻撃や情報漏洩のリスクがあります。
直し方: アプリ配下や専用ディレクトリを使う。
DEFAULT_ARCHIVE_LOG = Path(__file__).resolve().parent.parent / "archive.log"
一時ファイルが必要なら tempfile.mkstemp() を使います。
python:S3752 — HTTP メソッドの未指定
def evaluate_filter(request): # GET でも POST でも DELETE でも通る
...
Django の関数ビューはメソッドを制限しないと全部受け付けます。
直し方:
from django.views.decorators.http import require_GET
@require_GET
def evaluate_filter(request):
...
DRF の ViewSet や @action(methods=["get"]) を使っていれば出ません。
素の関数ビューを混ぜたときに出ます。
5. Code Smell(保守性)
python:S3776 / typescript:S3776 — 認知的複雑度
SonarQube で最も特徴的なルールです。 今回は Python 側で 28、
TypeScript 側で 22(上限 15)が出ました。
「行数」ではなく 「人間が読んで理解する難しさ」 を数値化します。
| 加点されるもの | 点 |
|---|---|
if / else if / else
|
+1 |
ループ(for / while) |
+1 |
catch / except
|
+1 |
&& / and の連鎖 |
+1 |
| ネストするごとに追加 | +ネストの深さ |
ネストが効くのが重要です。 同じ if でも、3 段ネストの中にあれば +4 になります。
直し方: 役割ごとに関数を分ける。今回は 1 つの関数を 3 つに分割しました。
# 修正前: 1 関数に全部(複雑度 28)
def summarize_todos(todos):
for todo in todos:
if todo.done: ...
elif ...:
if ...:
if ...: ...
# 修正後: 役割で分割(各関数は 15 未満)
def summarize_todos(todos):
for todo in todos:
_count_status(summary, todo)
_count_priority(summary, todo)
summary["warnings"].extend(_collect_warnings(todo))
閾値は変更できます(Quality Profile でルールのパラメータを編集)。
レガシーコードに導入して大量に出る場合は、一時的に 25 などに上げる運用もあります。
typescript:S4144 — 実装が同一の関数
コピペ検出です。 今回 legacy.ts の 3 つの関数で 2 件出ました。
export function exportOpenTodos(todos: Todo[]): string { /* 同じ中身 */ }
export function exportDoneTodos(todos: Todo[]): string { /* 同じ中身 */ }
export function exportArchivedTodos(todos: Todo[]): string { /* 同じ中身 */ }
メッセージは Update this function so that its implementation is not identical to the one on line 10.
直し方: 1 つにまとめ、差分は引数か呼び出し側に出す。
export function exportTodosAsCsv(todos: Todo[]): string { ... }
// 絞り込みは呼び出し側で: exportTodosAsCsv(todos.filter(t => !t.done))
Duplications メトリクスとは別物です。
| 見ているもの | |
|---|---|
S4144 |
関数単位で実装が同一か(指摘として出る) |
| Duplications | トークン列の重複率(メトリクスとして % で出る) |
今回は同じコードに対して両方反応し、S4144 2 件 と Duplications 10.9% が出ました。
python:S1192 — 重複した文字列リテラル
同じ文字列が 3 回以上出てきたら定数にすべき、というルールです。
lines = ["id,title,priority,done,created_at"] # 3 箇所で同じ文字列
直し方:
CSV_HEADER = "id,title,priority,done,created_at"
lines = [CSV_HEADER]
閾値は 3 回(パラメータで変更可能)。
typescript:S1854 — 使われない代入
代入したのに一度も読まれない変数です。
const unusedFilter = 'all' // どこでも使っていない
let description = todo.description.replace(/,/g, ' ') // 加工したが使っていない
直し方: 削除する。使うつもりだったなら使う。
python:S1481(未使用のローカル変数)が Python 側の相当ルールです。
typescript:S2486 — 例外を握り潰している
try {
return await response.json()
} catch (error) {
} // ← 何もしていない
メッセージは Handle this exception, don't catch it at all, or explain in a comment why it is ignored.
「処理するか、catch をやめるか、無視する理由をコメントに書け」 という 3 択を提示してきます。
直し方:
} catch (error) {
console.error('ToDo の取得に失敗しました', error)
throw error
}
Python の except Exception: pass に相当するルールは Sonar way に無く、
今回 backend 側では検出されませんでした。 言語による非対称の例です。
typescript:S2699 — アサーションが無いテスト
BLOCKER。今回最も重要度が高い Code Smell でした。
it('タイトル未設定でも例外にならない', () => {
describePriority(makeTodo({ title: '' })) // expect が無い = 必ず成功する
})
何も検証していないので、常に成功します。 「テストがある」と誤認させる点で
テストが無いより危険です。
直し方: 何を確認したいのか明示する。
it('タイトル未設定でも例外にならない', () => {
expect(() => describePriority(makeTodo({ title: '' }))).not.toThrow()
})
テストファイルとして認識されていないと出ません。
sonar.tests と sonar.test.inclusions の設定が効いている証拠にもなります。
python:S1135 / typescript:S1135 — TODO コメント
# TODO: 優先度の重み付けを設定ファイルから読み込むようにする
深刻度は INFO です。「消せ」ではなく「放置するな」という趣旨です。
FIXME は別ルール(S1134)で、こちらは深刻度が高めです。
運用の勘所: TODO を大量に抱えるプロジェクトでは、このルールを無効にするか
Quality Gate の対象から外さないと、常に不合格になります。
python:S125 / typescript:S125 — コメントアウトされたコード
今回、日本語コメントを誤検知しました。
# 環境変数から読み、無ければ実行ごとに使い捨ての鍵を生成する。
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") or get_random_secret_key()
上のコメント行が「コメントアウトされたコード」と判定されました。
このルールは英語前提でコードらしさを判定するため、日本語の文章が
引っかかることがあります。日本語でコメントを書く現場では避けられません。
対処:
| 状況 | 対処 |
|---|---|
| 単発 | False Positive としてマークし、理由をコメントに残す |
| 大量に出る | Quality Profile で python:S125 を無効化 |
やってはいけないのは、指摘を消すためにコメントを削る/英語に直すことです。
なお typescript:S125 は Sonar way で無効なので、TypeScript 側では出ません。
typescript:S106 — 標準出力への出力(console.log)
Sonar way では無効です。今回は有効化して検出しました。
console.log('loaded todos', todos.value.length)
直し方: ロガーを使う、または削除する。
注意点: javascript:S106 と typescript:S106 は別のルールです。
.js と .ts が混在するプロジェクトでは両方の言語プロファイルで有効化が必要です。
typescript:S4204 — any の使用
Sonar way では無効です。今回は有効化して 3 件検出しました。
const payload: any = await fetchTodos()
export async function fetchTodos(): Promise<any> { ... }
直し方: 型を定義する。
interface TodoListResponse {
count: number
results: Todo[]
}
export async function fetchTodos(): Promise<TodoListResponse> { ... }
ESLint の @typescript-eslint/no-explicit-any と同じ内容なので、
どちらで止めるかはチームで決めるべきです。
typescript:S7781 — replaceAll() を使うべき
todo.title.replace(/,/g, ' ') // 指摘される
todo.title.replaceAll(',', ' ') // 推奨
比較的新しいルールで、今回 8 件出て最多でした。正規表現の g フラグより
replaceAll の方が意図が明確、という趣旨です。
ES2021 以降が必要なので、古いブラウザを対象にする場合は無効化を検討します。
6. 「出なかった」ルール
ルール自体が存在しないもの
| 仕込んだもの | 確認方法と結果 |
|---|---|
v-html による XSS |
q='v-html' → TypeScript/JS に該当なし |
ALLOWED_HOSTS = ["*"] |
q='ALLOWED_HOSTS' → 0 件
|
eval() の使用(Python) |
q='eval' → 無関係なルールのみ |
v-html は Vue3 で最も危険な記述ですが、SonarQube では検出されません。
ESLint の vue/no-v-html で止める必要があります。
<!-- SonarQube は何も言わない -->
<div v-html="message" />
存在するが Sonar way で無効なもの
| ルール | 内容 |
|---|---|
typescript:S106 / javascript:S106
|
console.log の残留 |
typescript:S4204 |
any の使用 |
typescript:S1440 |
== ではなく === を使う |
typescript:S125 |
コメントアウトされたコード |
python:S1128 |
未使用の import
|
Quality Profile をコピーして有効化すれば検出できます。
有効なのに検出されなかったもの
| ルール | 理由 |
|---|---|
typescript:S2068(資格情報のハードコード) |
変数名が password / passwd / pwd などのパターンに合致するものだけを見るため、API_TOKEN は対象外。パラメータで検出語を追加できます
|
typescript:S1440(==) |
無効であることに加え、型情報から両辺が string と分かる場合は安全と判断される
|
Edition の制約で検出できないもの
| 種類 | 必要な Edition |
|---|---|
| SQL インジェクション、XSS、コマンド注入(taint analysis) | Developer Edition 以上 |
| 依存ライブラリの既知脆弱性(SCA) | Enterprise Edition + Advanced Security |
7. ルールの調べ方
画面から
上部メニュー Rules。
- 検索窓に ID(
S106)またはキーワード(console)を入れる - 左のフィルタで言語・種別・深刻度を絞る
- ルールを開くと
Why is this an issue?に Compliant / Non-compliant のコード例がある - 同じ画面でどの Quality Profile で有効かが分かる(空なら無効)
Why is this an issue? を読む習慣が最も重要です。 ID を暗記する必要はありません。
API から
# ルールが存在するか・有効か
curl -s -u "admin:$PASS" \
"$SONAR_URL/api/rules/show?key=typescript:S106&actives=true"
# → rule は返るが actives が空 = 存在するが無効
# キーワードで検索
curl -s -u "admin:$PASS" \
"$SONAR_URL/api/rules/search?q=console&languages=js,ts"
このリポジトリには一括確認用のスクリプトがあります。
docker compose run --rm --no-deps backend python ../scripts/rulecheck.py
公式ドキュメント
ルールの一覧は Sonar のルールサイトで言語別に読めます。
https://rules.sonarsource.com/python/RSPEC-3776 のように、RSPEC-<番号> で
直接引けます(S3776 → RSPEC-3776)。