177 lines
6.0 KiB
Python
177 lines
6.0 KiB
Python
"""Security regression tests for P0 remediations."""
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import jwt
|
|
import pytest
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from fastapi import HTTPException
|
|
from pydantic import ValidationError
|
|
from sqlalchemy import create_engine, text
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from app.core.errors import internal_error
|
|
from app.core.credentials import CREDENTIAL_PREFIX, decrypt_credential, encrypt_credential
|
|
from app.core.logging_utils import redact_log_message
|
|
from app.core.security import verify_casdoor_token
|
|
from app.middleware.audit_middleware import sanitize_audit_params
|
|
|
|
|
|
def test_production_rejects_disabled_imc_tls_verification():
|
|
from app.core.config import Settings
|
|
|
|
with pytest.raises(ValidationError, match="IMC_API_VERIFY_SSL"):
|
|
Settings(
|
|
SECRET_KEY="test-secret",
|
|
CREDENTIAL_ENCRYPTION_KEY="Q6NgOwoM3sna4ti4UeEo3Lo2cIzxc8wJMvMZ2cmw7lU=",
|
|
DATABASE_URL="sqlite:///./test.db",
|
|
REDIS_URL="redis://localhost:6379/15",
|
|
CASDOOR_ENDPOINT="https://casdoor.example.test",
|
|
CASDOOR_CLIENT_ID="test-client",
|
|
CASDOOR_CLIENT_SECRET="test-client-secret",
|
|
CASDOOR_ORG_NAME="test-org",
|
|
CASDOOR_APP_NAME="test-app",
|
|
IMC_API_URL="https://imc.example.test",
|
|
IMC_API_VERIFY_SSL=False,
|
|
DEBUG=False,
|
|
)
|
|
|
|
|
|
def test_audit_params_redacts_nested_sensitive_values():
|
|
params = {
|
|
"corpsecret": "corp-secret",
|
|
"profile": {"access_token": "access-token", "name": "operator"},
|
|
"items": [{"password": "device-password"}],
|
|
"normal": "kept",
|
|
}
|
|
|
|
assert sanitize_audit_params(params) == {
|
|
"corpsecret": "***",
|
|
"profile": {"access_token": "***", "name": "operator"},
|
|
"items": [{"password": "***"}],
|
|
"normal": "kept",
|
|
}
|
|
|
|
|
|
def test_internal_error_does_not_expose_exception_text():
|
|
exception = internal_error("test operation", RuntimeError("database password=should-not-leak"))
|
|
|
|
assert isinstance(exception, HTTPException)
|
|
assert exception.status_code == 500
|
|
assert exception.detail["code"] == "INTERNAL_ERROR"
|
|
assert "should-not-leak" not in str(exception.detail)
|
|
assert exception.detail["error_id"]
|
|
|
|
|
|
def test_log_redaction_masks_common_credential_formats():
|
|
message = "Authorization: Bearer abc.def password=hunter2&access_token=token-value corpsecret: corp-secret"
|
|
redacted = redact_log_message(message)
|
|
|
|
assert "abc.def" not in redacted
|
|
assert "hunter2" not in redacted
|
|
assert "token-value" not in redacted
|
|
assert "corp-secret" not in redacted
|
|
|
|
|
|
def test_olt_credential_encryption_round_trip(monkeypatch):
|
|
from cryptography.fernet import Fernet
|
|
from app.core.config import settings
|
|
|
|
monkeypatch.setattr(settings, "CREDENTIAL_ENCRYPTION_KEY", Fernet.generate_key().decode())
|
|
encrypted = encrypt_credential("olt-device-password")
|
|
|
|
assert encrypted.startswith(CREDENTIAL_PREFIX)
|
|
assert "olt-device-password" not in encrypted
|
|
assert decrypt_credential(encrypted) == "olt-device-password"
|
|
|
|
|
|
def test_olt_response_never_contains_device_password():
|
|
from app.schemas.olt import serialize_olt
|
|
from app.models.device import OLTDevice
|
|
|
|
device = OLTDevice(
|
|
id=1,
|
|
ip_address="10.0.0.1",
|
|
username="operator",
|
|
password="olt-device-password",
|
|
slot_command="display onu slot",
|
|
region="城区",
|
|
)
|
|
|
|
response = serialize_olt(device)
|
|
assert "password" not in response
|
|
assert "olt-device-password" not in str(response)
|
|
|
|
|
|
def test_olt_password_is_encrypted_in_database(monkeypatch):
|
|
from cryptography.fernet import Fernet
|
|
from app.core.config import settings
|
|
from app.core.database import Base
|
|
from app.models.device import OLTDevice
|
|
|
|
monkeypatch.setattr(settings, "CREDENTIAL_ENCRYPTION_KEY", Fernet.generate_key().decode())
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
session = sessionmaker(bind=engine)()
|
|
session.add(
|
|
OLTDevice(
|
|
id=1,
|
|
ip_address="10.0.0.1",
|
|
username="operator",
|
|
password="olt-device-password",
|
|
slot_command="display onu slot",
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
stored_password = session.execute(text("SELECT password FROM olt_devices")).scalar_one()
|
|
assert stored_password.startswith(CREDENTIAL_PREFIX)
|
|
assert "olt-device-password" not in stored_password
|
|
|
|
session.expire_all()
|
|
assert session.query(OLTDevice).one().password == "olt-device-password"
|
|
|
|
|
|
def test_verify_casdoor_token_requires_expected_signature_and_claims(monkeypatch):
|
|
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
public_pem = private_key.public_key().public_bytes(
|
|
serialization.Encoding.PEM,
|
|
serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
).decode()
|
|
now = datetime.now(timezone.utc)
|
|
|
|
from app.core.config import settings
|
|
|
|
monkeypatch.setattr(settings, "CASDOOR_CERTIFICATE", public_pem)
|
|
monkeypatch.setattr(settings, "CASDOOR_ENDPOINT", "https://casdoor.example.test")
|
|
monkeypatch.setattr(settings, "CASDOOR_ISSUER", "")
|
|
monkeypatch.setattr(settings, "CASDOOR_CLIENT_ID", "h3c-client")
|
|
|
|
token = jwt.encode(
|
|
{
|
|
"sub": "user-1",
|
|
"iss": "https://casdoor.example.test",
|
|
"aud": "h3c-client",
|
|
"iat": now,
|
|
"exp": now + timedelta(minutes=5),
|
|
},
|
|
private_key,
|
|
algorithm="RS256",
|
|
)
|
|
|
|
assert verify_casdoor_token(token)["sub"] == "user-1"
|
|
|
|
invalid_token = jwt.encode(
|
|
{
|
|
"sub": "user-1",
|
|
"iss": "https://casdoor.example.test",
|
|
"aud": "other-client",
|
|
"iat": now,
|
|
"exp": now + timedelta(minutes=5),
|
|
},
|
|
private_key,
|
|
algorithm="RS256",
|
|
)
|
|
with pytest.raises(jwt.InvalidAudienceError):
|
|
verify_casdoor_token(invalid_token)
|