python / test_review.py
python / test_review.py
import json
from pathlib import Path
import httpx
from jsonschema import Draft202012Validator
import server
from review import build_review
def sample(headers=None, html="", url="https://example.com/"):
response = httpx.Response(200, headers=headers or {"content-type": "text/html"},
text=html, request=httpx.Request("GET", url))
return server.analyze_response(response, url, [url], include_review_markdown=True)
def test_preload_token_is_not_registration_or_browser_enforcement():
result = sample({"strict-transport-security": "max-age=31536000; includeSubDomains; preload"})
hsts = result["headers"]["strict-transport-security"]
assert hsts["preload_token_observed"] is True
assert hsts["include_subdomains_token_observed"] is True
assert hsts["max_age_candidate"] == 31536000
assert hsts["preload_list_status"] == "not_checked"
assert hsts["browser_enforcement"] == "not_verified"
assert "初回HTTPアクセスの保護は未確認" in result["review_markdown"]
def test_preload_word_in_a_value_is_not_a_directive():
result = sample({"strict-transport-security": "max-age=31536000; note=preload"})
assert result["headers"]["strict-transport-security"]["preload_token_observed"] is False
absent = sample()["headers"]["strict-transport-security"]
assert absent["present"] is False
assert absent["preload_list_status"] == "not_checked"
def test_sri_attributes_never_imply_working_beacon_or_trusted_bytes():
result = sample(html='<script src="https://static.cloudflareinsights.com/beacon.js" integrity="sha512-AAAA" crossorigin="anonymous"></script>')
sri = result["html"]["sri_observations"][0]
assert sri["integrity_syntax_candidate"] is True
assert sri["hash_verified"] is False
assert sri["cors_eligibility"] == sri["browser_load"] == sri["browser_execution"] == "not_verified"
assert sri["resource_compromise"] == "not_checked"
assert "実際のハッシュ一致" in result["review_markdown"]
assert "実運用で機能している" not in result["review_markdown"]
def test_login_form_is_a_declaration_not_a_verified_endpoint():
result = sample(html='<form method="POST" action="/customer/login"><input type="password"><input type="hidden" name="csrf" value="PRIVATE_FORM_SENTINEL_123"></form>')
form = result["html"]["forms"][0]
assert form["action"] == "/customer/login" and form["has_password"] is True
assert form["role_inference"] == "possibly_login_related"
assert form["endpoint_reachability"] == "not_tested"
assert form["authentication_behavior"] == form["state_change_behavior"] == "not_verified"
assert form["csrf_controls"] == "not_inspected"
assert "PRIVATE_FORM_SENTINEL_123" not in json.dumps(result)
assert "フォーム宣言(処理内容は未検証)" in result["review_markdown"]
def test_priority_uses_severity_not_conditional_impact_or_asset_value():
result = sample()
base = result["findings"][0]
result["findings"] = [dict(base, severity="low", conditional_impact={"level": "critical", "description": "conditional"}),
dict(base, severity="medium", conditional_impact={"level": "low", "description": "conditional"}),
dict(base, severity="low", conditional_impact={"level": "high", "description": "conditional"})]
review, markdown = build_review(result)
assert [r["finding_index"] for r in review["finding_rows"]] == [1, 0, 2]
assert "severity_only" in review["ordering_basis"]
assert "severity(設定上の優先度)" in markdown
assert "conditional_impact(条件成立時)" in markdown
def test_all_findings_conditions_and_levels_preserved_in_report():
result = sample({"content-type": "text/html", "set-cookie": "PHPSESSID=SECRET"},
'<script src="https://cdn.jsdelivr.net/npm/swiper@12/a.js"></script>')
rows = result["review"]["finding_rows"]
assert len(rows) == len(result["findings"])
assert len({r["finding_index"] for r in rows}) == len(rows)
for row in rows:
finding = result["findings"][row["finding_index"]]
assert row["severity"] == finding["severity"]
assert row["conditional_impact_level"] == finding["conditional_impact"]["level"]
assert row["conditions_status"] == "not_verified"
for condition in finding["required_conditions"]:
assert condition in result["review_markdown"]
schema = json.loads(Path(__file__).with_name("finding.schema.json").read_text())
for finding in result["findings"]:
Draft202012Validator(schema).validate(finding)
assert "SECRET" not in json.dumps(result)
def test_cookie_exposure_separate_from_sensitive_use_and_takeover():
result = sample({"set-cookie": "PHPSESSID=x"})
finding = next(f for f in result["findings"] if f["finding"] == "cookie_missing_httponly")
assert len(finding["required_conditions"]) == 2
assert all("authentication state" not in c for c in finding["required_conditions"])
assert "account takeover additionally requires" in finding["conditional_impact"]["description"]
assert finding["evidence"]["hsts_preload_list_status"] == "not_checked"
def test_observed_markup_cannot_escape_report_tables_or_json_blocks():
attack = 'x|[click](https://evil.test)<script>```\n# injected'
result = sample({"server": attack, "content-type": "text/html"},
'<form action="/x|[click](https://evil.test)<script>"><input type="password"></form>')
markdown = result["review_markdown"]
assert "[click](https://evil.test)" not in markdown.split("### フォーム宣言")[1].split("## 設定上")[0]
assert "<script>" not in markdown
assert "\n# injected" not in markdown
assert markdown.count("```json") == markdown.count("\n```\n")
def test_report_is_repeatable_and_no_unverified_provider_claims_added():
result = sample(html='<form action="/cart/proxy"><input type="password"></form>')
first = build_review(result)
second = build_review(result)
assert first == second
assert "Shopify" not in first[1]
assert "Google Fonts" not in first[1]
assert "最後の防御層" not in first[1]
def test_non_html_still_has_review_and_no_endpoint_claims():
result = sample({"content-type": "application/json"}, '{"ok": true}')
assert result["html"]["is_html"] is False
assert result["review"]["finding_rows"]
assert result["version"] == "0.6.0"
def test_compact_default_preserves_complete_findings_and_review():
url = "https://example.com/"
response = httpx.Response(200, headers={"content-type": "text/html"},
text='<script src="https://cdn.example/a.js"></script>',
request=httpx.Request("GET", url))
compact = server.analyze_response(response, url, [url])
full = server.analyze_response(response, url, [url], include_review_markdown=True)
assert "review_markdown" not in compact
assert compact["review_markdown_included"] is False
assert compact["findings"] == full["findings"]
assert compact["review"] == full["review"]
assert len(json.dumps(compact)) < len(json.dumps(full)) * 0.7
assert all(row["standalone_status_ja"] == "未実証" for row in compact["review"]["finding_rows"])
def test_identical_script_occurrences_merge_without_double_penalty():
tag = '<script src="https://cdn.jsdelivr.net/npm/swiper@12/a.js"></script>'
single = sample(html=tag)
doubled = sample(html=tag + tag)
assert doubled["html"]["script_count"] == 2
assert doubled["security_score"] == single["security_score"]
assert len(doubled["findings"]) == len(single["findings"])
scripts = [f for f in doubled["findings"] if f["category"] == "supply-chain"]
assert len(scripts) == 2 # SRI and mutable URL remain distinct issues.
for finding in scripts:
assert finding["evidence"]["occurrence_count"] == 2
assert finding["evidence"]["script_indexes"] == [0, 1]
assert doubled["html"]["sri_observations"][0]["occurrence_count"] == 2
def test_same_resource_with_different_security_attributes_stays_distinct():
result = sample(html='<script src="https://cdn.example/a.js"></script>'
'<script src="https://cdn.example/a.js" integrity="bad"></script>'
'<script src="https://cdn.example/a.js" integrity="sha256-AAAA" crossorigin="anonymous"></script>')
assert len(result["html"]["sri_observations"]) == 3
assert all(o["occurrence_count"] == 1 for o in result["html"]["sri_observations"])
assert len([f for f in result["findings"] if f["category"] == "supply-chain"]) == 2
def test_dynamic_script_advice_does_not_assume_self_hosting_supported():
result = sample(html='<script src="https://www.googletagmanager.com/gtag/js?id=TEST"></script>')
finding = next(f for f in result["findings"] if f["finding"] == "sri_missing")
assert "official support" in finding["recommendation"]
assert "do not copy or self-host them by default" in finding["recommendation"]
def test_samesite_none_declares_attribute_not_confirmed_intent():
result = sample({"set-cookie": "unknown=x; Secure; HttpOnly; SameSite=None"})
finding = next(f for f in result["findings"] if f["finding"] == "cookie_samesite_none")
assert "unverified" in finding["inference"]
row = next(r for r in result["review"]["finding_rows"] if r["finding"] == "cookie_samesite_none")
assert "用途・意図は未確認" in row["title_ja"]
def test_base_like_frame_only_csp_cookie_and_duplicate_scripts():
# Synthetic reconstruction of the reported combination, without live requests.
sources = ["https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
"https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
"https://assets.example/pinit.js",
"https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js",
"https://cdn.jsdelivr.net/npm/intersection-observer@0.10.0/intersection-observer.js",
"https://assets.example/base.js", "https://apps.example/message.js"]
result = sample({"content-type": "text/html", "server": "nginx",
"content-security-policy": "frame-ancestors 'self' https://admin.thebase.com",
"set-cookie": "THEBASE_V1=synthetic; Secure; HttpOnly; SameSite=None"},
''.join(f'<script src="{src}"></script>' for src in sources))
codes = [f["finding"] for f in result["findings"]]
assert "csp_script_restriction_missing" in codes
assert "framing_restriction_missing_or_weak" not in codes
assert not any(code.startswith("cookie_missing") for code in codes)
assert codes.count("sri_missing") == 6
assert result["html"]["script_count"] == 7
assert len(result["findings"]) == 17
assert result["security_score"] == 63
assert result["score_method_version"] == "2"
def test_sri_and_mutable_url_have_separate_condition_groups_and_complete_conditions():
result = sample(html='<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>')
rows = {row["finding"]: row for row in result["review"]["finding_rows"]}
assert rows["sri_missing"]["condition_group_id"] != rows["sri_mutable_url"]["condition_group_id"]
for code in ["sri_missing", "sri_mutable_url"]:
finding = result["findings"][rows[code]["finding_index"]]
assert rows[code]["required_conditions_count"] == len(finding["required_conditions"]) == 2
assert all(condition in result["review_markdown"] for condition in finding["required_conditions"])
assert result["review"]["grouping_policy"]["contiguous_rows_only"] is True
assert "URL-change/application-dependency" in ' '.join(result["reporting_rules"])
def test_common_sri_conditions_can_group_but_different_impacts_cannot():
result = sample(html='<script src="https://cdn.example/a.js"></script><script src="https://cdn.example/b.js"></script>')
rows = [r for r in result["review"]["finding_rows"] if r["finding"] == "sri_missing"]
assert rows[0]["condition_group_id"] == rows[1]["condition_group_id"]
changed = result["findings"][rows[1]["finding_index"]]
changed["conditional_impact"] = {"level": "low", "description": "different impact"}
review, _ = build_review(result, render_markdown=False)
rows = [r for r in review["finding_rows"] if r["finding"] == "sri_missing"]
assert rows[0]["condition_group_id"] != rows[1]["condition_group_id"]
assert review["translation_glossary"]["executable"] == "実行可能"
import json
from pathlib import Path
import httpx
from jsonschema import Draft202012Validator
import server
from review import build_review
def sample(headers=None, html="", url="https://example.com/"):
response = httpx.Response(200, headers=headers or {"content-type": "text/html"},
text=html, request=httpx.Request("GET", url))
return server.analyze_response(response, url, [url], include_review_markdown=True)
def test_preload_token_is_not_registration_or_browser_enforcement():
result = sample({"strict-transport-security": "max-age=31536000; includeSubDomains; preload"})
hsts = result["headers"]["strict-transport-security"]
assert hsts["preload_token_observed"] is True
assert hsts["include_subdomains_token_observed"] is True
assert hsts["max_age_candidate"] == 31536000
assert hsts["preload_list_status"] == "not_checked"
assert hsts["browser_enforcement"] == "not_verified"
assert "初回HTTPアクセスの保護は未確認" in result["review_markdown"]
def test_preload_word_in_a_value_is_not_a_directive():
result = sample({"strict-transport-security": "max-age=31536000; note=preload"})
assert result["headers"]["strict-transport-security"]["preload_token_observed"] is False
absent = sample()["headers"]["strict-transport-security"]
assert absent["present"] is False
assert absent["preload_list_status"] == "not_checked"
def test_sri_attributes_never_imply_working_beacon_or_trusted_bytes():
result = sample(html='<script src="https://static.cloudflareinsights.com/beacon.js" integrity="sha512-AAAA" crossorigin="anonymous"></script>')
sri = result["html"]["sri_observations"][0]
assert sri["integrity_syntax_candidate"] is True
assert sri["hash_verified"] is False
assert sri["cors_eligibility"] == sri["browser_load"] == sri["browser_execution"] == "not_verified"
assert sri["resource_compromise"] == "not_checked"
assert "実際のハッシュ一致" in result["review_markdown"]
assert "実運用で機能している" not in result["review_markdown"]
def test_login_form_is_a_declaration_not_a_verified_endpoint():
result = sample(html='<form method="POST" action="/customer/login"><input type="password"><input type="hidden" name="csrf" value="PRIVATE_FORM_SENTINEL_123"></form>')
form = result["html"]["forms"][0]
assert form["action"] == "/customer/login" and form["has_password"] is True
assert form["role_inference"] == "possibly_login_related"
assert form["endpoint_reachability"] == "not_tested"
assert form["authentication_behavior"] == form["state_change_behavior"] == "not_verified"
assert form["csrf_controls"] == "not_inspected"
assert "PRIVATE_FORM_SENTINEL_123" not in json.dumps(result)
assert "フォーム宣言(処理内容は未検証)" in result["review_markdown"]
def test_priority_uses_severity_not_conditional_impact_or_asset_value():
result = sample()
base = result["findings"][0]
result["findings"] = [dict(base, severity="low", conditional_impact={"level": "critical", "description": "conditional"}),
dict(base, severity="medium", conditional_impact={"level": "low", "description": "conditional"}),
dict(base, severity="low", conditional_impact={"level": "high", "description": "conditional"})]
review, markdown = build_review(result)
assert [r["finding_index"] for r in review["finding_rows"]] == [1, 0, 2]
assert "severity_only" in review["ordering_basis"]
assert "severity(設定上の優先度)" in markdown
assert "conditional_impact(条件成立時)" in markdown
def test_all_findings_conditions_and_levels_preserved_in_report():
result = sample({"content-type": "text/html", "set-cookie": "PHPSESSID=SECRET"},
'<script src="https://cdn.jsdelivr.net/npm/swiper@12/a.js"></script>')
rows = result["review"]["finding_rows"]
assert len(rows) == len(result["findings"])
assert len({r["finding_index"] for r in rows}) == len(rows)
for row in rows:
finding = result["findings"][row["finding_index"]]
assert row["severity"] == finding["severity"]
assert row["conditional_impact_level"] == finding["conditional_impact"]["level"]
assert row["conditions_status"] == "not_verified"
for condition in finding["required_conditions"]:
assert condition in result["review_markdown"]
schema = json.loads(Path(__file__).with_name("finding.schema.json").read_text())
for finding in result["findings"]:
Draft202012Validator(schema).validate(finding)
assert "SECRET" not in json.dumps(result)
def test_cookie_exposure_separate_from_sensitive_use_and_takeover():
result = sample({"set-cookie": "PHPSESSID=x"})
finding = next(f for f in result["findings"] if f["finding"] == "cookie_missing_httponly")
assert len(finding["required_conditions"]) == 2
assert all("authentication state" not in c for c in finding["required_conditions"])
assert "account takeover additionally requires" in finding["conditional_impact"]["description"]
assert finding["evidence"]["hsts_preload_list_status"] == "not_checked"
def test_observed_markup_cannot_escape_report_tables_or_json_blocks():
attack = 'x|[click](https://evil.test)<script>```\n# injected'
result = sample({"server": attack, "content-type": "text/html"},
'<form action="/x|[click](https://evil.test)<script>"><input type="password"></form>')
markdown = result["review_markdown"]
assert "[click](https://evil.test)" not in markdown.split("### フォーム宣言")[1].split("## 設定上")[0]
assert "<script>" not in markdown
assert "\n# injected" not in markdown
assert markdown.count("```json") == markdown.count("\n```\n")
def test_report_is_repeatable_and_no_unverified_provider_claims_added():
result = sample(html='<form action="/cart/proxy"><input type="password"></form>')
first = build_review(result)
second = build_review(result)
assert first == second
assert "Shopify" not in first[1]
assert "Google Fonts" not in first[1]
assert "最後の防御層" not in first[1]
def test_non_html_still_has_review_and_no_endpoint_claims():
result = sample({"content-type": "application/json"}, '{"ok": true}')
assert result["html"]["is_html"] is False
assert result["review"]["finding_rows"]
assert result["version"] == "0.6.0"
def test_compact_default_preserves_complete_findings_and_review():
url = "https://example.com/"
response = httpx.Response(200, headers={"content-type": "text/html"},
text='<script src="https://cdn.example/a.js"></script>',
request=httpx.Request("GET", url))
compact = server.analyze_response(response, url, [url])
full = server.analyze_response(response, url, [url], include_review_markdown=True)
assert "review_markdown" not in compact
assert compact["review_markdown_included"] is False
assert compact["findings"] == full["findings"]
assert compact["review"] == full["review"]
assert len(json.dumps(compact)) < len(json.dumps(full)) * 0.7
assert all(row["standalone_status_ja"] == "未実証" for row in compact["review"]["finding_rows"])
def test_identical_script_occurrences_merge_without_double_penalty():
tag = '<script src="https://cdn.jsdelivr.net/npm/swiper@12/a.js"></script>'
single = sample(html=tag)
doubled = sample(html=tag + tag)
assert doubled["html"]["script_count"] == 2
assert doubled["security_score"] == single["security_score"]
assert len(doubled["findings"]) == len(single["findings"])
scripts = [f for f in doubled["findings"] if f["category"] == "supply-chain"]
assert len(scripts) == 2 # SRI and mutable URL remain distinct issues.
for finding in scripts:
assert finding["evidence"]["occurrence_count"] == 2
assert finding["evidence"]["script_indexes"] == [0, 1]
assert doubled["html"]["sri_observations"][0]["occurrence_count"] == 2
def test_same_resource_with_different_security_attributes_stays_distinct():
result = sample(html='<script src="https://cdn.example/a.js"></script>'
'<script src="https://cdn.example/a.js" integrity="bad"></script>'
'<script src="https://cdn.example/a.js" integrity="sha256-AAAA" crossorigin="anonymous"></script>')
assert len(result["html"]["sri_observations"]) == 3
assert all(o["occurrence_count"] == 1 for o in result["html"]["sri_observations"])
assert len([f for f in result["findings"] if f["category"] == "supply-chain"]) == 2
def test_dynamic_script_advice_does_not_assume_self_hosting_supported():
result = sample(html='<script src="https://www.googletagmanager.com/gtag/js?id=TEST"></script>')
finding = next(f for f in result["findings"] if f["finding"] == "sri_missing")
assert "official support" in finding["recommendation"]
assert "do not copy or self-host them by default" in finding["recommendation"]
def test_samesite_none_declares_attribute_not_confirmed_intent():
result = sample({"set-cookie": "unknown=x; Secure; HttpOnly; SameSite=None"})
finding = next(f for f in result["findings"] if f["finding"] == "cookie_samesite_none")
assert "unverified" in finding["inference"]
row = next(r for r in result["review"]["finding_rows"] if r["finding"] == "cookie_samesite_none")
assert "用途・意図は未確認" in row["title_ja"]
def test_base_like_frame_only_csp_cookie_and_duplicate_scripts():
# Synthetic reconstruction of the reported combination, without live requests.
sources = ["https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
"https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
"https://assets.example/pinit.js",
"https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js",
"https://cdn.jsdelivr.net/npm/intersection-observer@0.10.0/intersection-observer.js",
"https://assets.example/base.js", "https://apps.example/message.js"]
result = sample({"content-type": "text/html", "server": "nginx",
"content-security-policy": "frame-ancestors 'self' https://admin.thebase.com",
"set-cookie": "THEBASE_V1=synthetic; Secure; HttpOnly; SameSite=None"},
''.join(f'<script src="{src}"></script>' for src in sources))
codes = [f["finding"] for f in result["findings"]]
assert "csp_script_restriction_missing" in codes
assert "framing_restriction_missing_or_weak" not in codes
assert not any(code.startswith("cookie_missing") for code in codes)
assert codes.count("sri_missing") == 6
assert result["html"]["script_count"] == 7
assert len(result["findings"]) == 17
assert result["security_score"] == 63
assert result["score_method_version"] == "2"
def test_sri_and_mutable_url_have_separate_condition_groups_and_complete_conditions():
result = sample(html='<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>')
rows = {row["finding"]: row for row in result["review"]["finding_rows"]}
assert rows["sri_missing"]["condition_group_id"] != rows["sri_mutable_url"]["condition_group_id"]
for code in ["sri_missing", "sri_mutable_url"]:
finding = result["findings"][rows[code]["finding_index"]]
assert rows[code]["required_conditions_count"] == len(finding["required_conditions"]) == 2
assert all(condition in result["review_markdown"] for condition in finding["required_conditions"])
assert result["review"]["grouping_policy"]["contiguous_rows_only"] is True
assert "URL-change/application-dependency" in ' '.join(result["reporting_rules"])
def test_common_sri_conditions_can_group_but_different_impacts_cannot():
result = sample(html='<script src="https://cdn.example/a.js"></script><script src="https://cdn.example/b.js"></script>')
rows = [r for r in result["review"]["finding_rows"] if r["finding"] == "sri_missing"]
assert rows[0]["condition_group_id"] == rows[1]["condition_group_id"]
changed = result["findings"][rows[1]["finding_index"]]
changed["conditional_impact"] = {"level": "low", "description": "different impact"}
review, _ = build_review(result, render_markdown=False)
rows = [r for r in review["finding_rows"] if r["finding"] == "sri_missing"]
assert rows[0]["condition_group_id"] != rows[1]["condition_group_id"]
assert review["translation_glossary"]["executable"] == "実行可能"