fix(security): complete security and delivery compliance remediation
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user