nanopy-bank 1.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nanopy_bank/__init__.py +20 -0
- nanopy_bank/api/__init__.py +10 -0
- nanopy_bank/api/server.py +242 -0
- nanopy_bank/app.py +282 -0
- nanopy_bank/cli.py +152 -0
- nanopy_bank/core/__init__.py +85 -0
- nanopy_bank/core/audit.py +404 -0
- nanopy_bank/core/auth.py +306 -0
- nanopy_bank/core/bank.py +407 -0
- nanopy_bank/core/beneficiary.py +258 -0
- nanopy_bank/core/branch.py +319 -0
- nanopy_bank/core/fees.py +243 -0
- nanopy_bank/core/holding.py +416 -0
- nanopy_bank/core/models.py +308 -0
- nanopy_bank/core/products.py +300 -0
- nanopy_bank/data/__init__.py +31 -0
- nanopy_bank/data/demo.py +846 -0
- nanopy_bank/documents/__init__.py +11 -0
- nanopy_bank/documents/statement.py +304 -0
- nanopy_bank/sepa/__init__.py +10 -0
- nanopy_bank/sepa/sepa.py +452 -0
- nanopy_bank/storage/__init__.py +11 -0
- nanopy_bank/storage/json_storage.py +127 -0
- nanopy_bank/storage/sqlite_storage.py +326 -0
- nanopy_bank/ui/__init__.py +14 -0
- nanopy_bank/ui/pages/__init__.py +33 -0
- nanopy_bank/ui/pages/accounts.py +85 -0
- nanopy_bank/ui/pages/advisor.py +140 -0
- nanopy_bank/ui/pages/audit.py +73 -0
- nanopy_bank/ui/pages/beneficiaries.py +115 -0
- nanopy_bank/ui/pages/branches.py +64 -0
- nanopy_bank/ui/pages/cards.py +36 -0
- nanopy_bank/ui/pages/common.py +18 -0
- nanopy_bank/ui/pages/dashboard.py +100 -0
- nanopy_bank/ui/pages/fees.py +60 -0
- nanopy_bank/ui/pages/holding.py +943 -0
- nanopy_bank/ui/pages/loans.py +105 -0
- nanopy_bank/ui/pages/login.py +174 -0
- nanopy_bank/ui/pages/sepa.py +118 -0
- nanopy_bank/ui/pages/settings.py +48 -0
- nanopy_bank/ui/pages/transfers.py +94 -0
- nanopy_bank/ui/pages.py +16 -0
- nanopy_bank-1.0.8.dist-info/METADATA +72 -0
- nanopy_bank-1.0.8.dist-info/RECORD +47 -0
- nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
- nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
- nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
nanopy_bank/cli.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NanoPy Bank CLI
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
@click.version_option(version="1.0.0")
|
|
12
|
+
def main():
|
|
13
|
+
"""NanoPy Bank - Online Banking System"""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@main.command()
|
|
18
|
+
@click.option("--port", "-p", default=8501, help="Port to run on")
|
|
19
|
+
@click.option("--host", "-h", default="localhost", help="Host to bind to")
|
|
20
|
+
def serve(port: int, host: str):
|
|
21
|
+
"""Start the banking UI (Streamlit)"""
|
|
22
|
+
import subprocess
|
|
23
|
+
|
|
24
|
+
app_path = os.path.join(os.path.dirname(__file__), "app.py")
|
|
25
|
+
click.echo(f"Starting NanoPy Bank on http://{host}:{port}")
|
|
26
|
+
|
|
27
|
+
subprocess.run([
|
|
28
|
+
sys.executable, "-m", "streamlit", "run", app_path,
|
|
29
|
+
"--server.port", str(port),
|
|
30
|
+
"--server.address", host,
|
|
31
|
+
"--theme.base", "dark"
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@main.command()
|
|
36
|
+
@click.option("--iban", "-i", required=True, help="Account IBAN")
|
|
37
|
+
@click.option("--output", "-o", default="statement.xml", help="Output file")
|
|
38
|
+
def export_statement(iban: str, output: str):
|
|
39
|
+
"""Export bank statement as SEPA XML (camt.053)"""
|
|
40
|
+
from .core import get_bank
|
|
41
|
+
from .sepa import SEPAGenerator
|
|
42
|
+
from datetime import datetime, timedelta
|
|
43
|
+
|
|
44
|
+
bank = get_bank()
|
|
45
|
+
account = bank.get_account(iban)
|
|
46
|
+
|
|
47
|
+
if not account:
|
|
48
|
+
click.echo(f"Account {iban} not found", err=True)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
transactions = bank.get_account_transactions(iban, limit=100)
|
|
52
|
+
|
|
53
|
+
generator = SEPAGenerator(
|
|
54
|
+
initiator_name="NanoPy Bank",
|
|
55
|
+
initiator_iban=iban,
|
|
56
|
+
initiator_bic=account.bic
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
xml_content = generator.generate_statement(
|
|
60
|
+
iban=iban,
|
|
61
|
+
transactions=transactions,
|
|
62
|
+
opening_balance=account.balance,
|
|
63
|
+
closing_balance=account.balance,
|
|
64
|
+
from_date=datetime.now() - timedelta(days=30),
|
|
65
|
+
to_date=datetime.now()
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
69
|
+
f.write(xml_content)
|
|
70
|
+
|
|
71
|
+
click.echo(f"Statement exported to {output}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@main.command()
|
|
75
|
+
@click.option("--iban", "-i", required=True, help="Source Account IBAN")
|
|
76
|
+
def export_sepa(iban: str):
|
|
77
|
+
"""Export SEPA Credit Transfer XML (pain.001)"""
|
|
78
|
+
click.echo("Use the web UI for SEPA exports: nanopy-bank serve")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@main.command()
|
|
82
|
+
def stats():
|
|
83
|
+
"""Show bank statistics"""
|
|
84
|
+
from .core import get_bank
|
|
85
|
+
|
|
86
|
+
bank = get_bank()
|
|
87
|
+
stats = bank.get_stats()
|
|
88
|
+
|
|
89
|
+
click.echo("\n=== NanoPy Bank Statistics ===\n")
|
|
90
|
+
click.echo(f"Customers: {stats['total_customers']}")
|
|
91
|
+
click.echo(f"Accounts: {stats['total_accounts']}")
|
|
92
|
+
click.echo(f"Transactions: {stats['total_transactions']}")
|
|
93
|
+
click.echo(f"Total Balance: {stats['total_balance']} EUR")
|
|
94
|
+
click.echo(f"Today's Txs: {stats['transactions_today']}")
|
|
95
|
+
click.echo()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@main.command()
|
|
99
|
+
def demo():
|
|
100
|
+
"""Create demo data"""
|
|
101
|
+
from decimal import Decimal
|
|
102
|
+
from .core import get_bank, AccountType, TransactionType
|
|
103
|
+
|
|
104
|
+
bank = get_bank()
|
|
105
|
+
|
|
106
|
+
click.echo("Creating demo customer and account...")
|
|
107
|
+
|
|
108
|
+
# Create customer
|
|
109
|
+
customer = bank.create_customer(
|
|
110
|
+
first_name="Marie",
|
|
111
|
+
last_name="Dupont",
|
|
112
|
+
email="marie.dupont@email.com",
|
|
113
|
+
phone="+33612345678",
|
|
114
|
+
address="45 Avenue des Champs-Élysées",
|
|
115
|
+
city="Paris",
|
|
116
|
+
postal_code="75008",
|
|
117
|
+
country="FR"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Create checking account
|
|
121
|
+
account = bank.create_account(
|
|
122
|
+
customer_id=customer.customer_id,
|
|
123
|
+
account_type=AccountType.CHECKING,
|
|
124
|
+
initial_balance=Decimal("2500.00"),
|
|
125
|
+
account_name="Compte Courant"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Create savings account
|
|
129
|
+
savings = bank.create_account(
|
|
130
|
+
customer_id=customer.customer_id,
|
|
131
|
+
account_type=AccountType.SAVINGS,
|
|
132
|
+
initial_balance=Decimal("15000.00"),
|
|
133
|
+
account_name="Livret A"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Add demo transactions
|
|
137
|
+
bank.credit(account.iban, Decimal("3200.00"), "Salaire Décembre", "ACME SARL", "", TransactionType.SEPA_CREDIT)
|
|
138
|
+
bank.debit(account.iban, Decimal("850.00"), "Loyer Janvier", "IMMO PARIS", "", TransactionType.SEPA_DEBIT)
|
|
139
|
+
bank.debit(account.iban, Decimal("127.50"), "EDF Electricité", "EDF", "", TransactionType.SEPA_DEBIT)
|
|
140
|
+
bank.debit(account.iban, Decimal("45.90"), "Carrefour Market", "CARREFOUR", "", TransactionType.CARD_PAYMENT)
|
|
141
|
+
bank.debit(account.iban, Decimal("12.99"), "Spotify Premium", "SPOTIFY", "", TransactionType.CARD_PAYMENT)
|
|
142
|
+
bank.debit(account.iban, Decimal("60.00"), "Retrait DAB", "", "", TransactionType.ATM_WITHDRAWAL)
|
|
143
|
+
|
|
144
|
+
click.echo(f"\nDemo created!")
|
|
145
|
+
click.echo(f"Customer: {customer.full_name} ({customer.customer_id})")
|
|
146
|
+
click.echo(f"Checking: {account.format_iban()}")
|
|
147
|
+
click.echo(f"Savings: {savings.format_iban()}")
|
|
148
|
+
click.echo(f"\nRun 'nanopy-bank serve' to access the UI")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core banking module
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .models import (
|
|
6
|
+
Account, Transaction, Customer, Card,
|
|
7
|
+
TransactionType, AccountType, AccountStatus, Currency,
|
|
8
|
+
CardType, CardStatus
|
|
9
|
+
)
|
|
10
|
+
from .bank import Bank, get_bank
|
|
11
|
+
from .beneficiary import (
|
|
12
|
+
Beneficiary, StandingOrder, SEPAMandate,
|
|
13
|
+
OrderFrequency, OrderStatus, MandateType, MandateStatus
|
|
14
|
+
)
|
|
15
|
+
from .products import (
|
|
16
|
+
Loan, Insurance, SavingsProduct,
|
|
17
|
+
LoanType, LoanStatus, InsuranceType, SavingsType
|
|
18
|
+
)
|
|
19
|
+
from .fees import Fee, InterestRate, AppliedFee, FeeType, RateType
|
|
20
|
+
from .branch import Branch, Employee, ATM, BranchType, EmployeeRole, EmployeeStatus
|
|
21
|
+
from .audit import (
|
|
22
|
+
AuditLog, SecurityEvent, ComplianceCheck, AuditLogger,
|
|
23
|
+
AuditAction, SecurityEventType, RiskLevel, EventStatus
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Models
|
|
28
|
+
"Account",
|
|
29
|
+
"Transaction",
|
|
30
|
+
"Customer",
|
|
31
|
+
"Card",
|
|
32
|
+
"TransactionType",
|
|
33
|
+
"AccountType",
|
|
34
|
+
"AccountStatus",
|
|
35
|
+
"Currency",
|
|
36
|
+
"CardType",
|
|
37
|
+
"CardStatus",
|
|
38
|
+
|
|
39
|
+
# Bank
|
|
40
|
+
"Bank",
|
|
41
|
+
"get_bank",
|
|
42
|
+
|
|
43
|
+
# Beneficiary
|
|
44
|
+
"Beneficiary",
|
|
45
|
+
"StandingOrder",
|
|
46
|
+
"SEPAMandate",
|
|
47
|
+
"OrderFrequency",
|
|
48
|
+
"OrderStatus",
|
|
49
|
+
"MandateType",
|
|
50
|
+
"MandateStatus",
|
|
51
|
+
|
|
52
|
+
# Products
|
|
53
|
+
"Loan",
|
|
54
|
+
"Insurance",
|
|
55
|
+
"SavingsProduct",
|
|
56
|
+
"LoanType",
|
|
57
|
+
"LoanStatus",
|
|
58
|
+
"InsuranceType",
|
|
59
|
+
"SavingsType",
|
|
60
|
+
|
|
61
|
+
# Fees
|
|
62
|
+
"Fee",
|
|
63
|
+
"InterestRate",
|
|
64
|
+
"AppliedFee",
|
|
65
|
+
"FeeType",
|
|
66
|
+
"RateType",
|
|
67
|
+
|
|
68
|
+
# Branch
|
|
69
|
+
"Branch",
|
|
70
|
+
"Employee",
|
|
71
|
+
"ATM",
|
|
72
|
+
"BranchType",
|
|
73
|
+
"EmployeeRole",
|
|
74
|
+
"EmployeeStatus",
|
|
75
|
+
|
|
76
|
+
# Audit
|
|
77
|
+
"AuditLog",
|
|
78
|
+
"SecurityEvent",
|
|
79
|
+
"ComplianceCheck",
|
|
80
|
+
"AuditLogger",
|
|
81
|
+
"AuditAction",
|
|
82
|
+
"SecurityEventType",
|
|
83
|
+
"RiskLevel",
|
|
84
|
+
"EventStatus",
|
|
85
|
+
]
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit and Security Models - Logging, Events, Compliance
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional, Dict, Any
|
|
10
|
+
import uuid
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuditAction(Enum):
|
|
16
|
+
"""Types of auditable actions"""
|
|
17
|
+
# Account actions
|
|
18
|
+
ACCOUNT_CREATE = "account_create"
|
|
19
|
+
ACCOUNT_UPDATE = "account_update"
|
|
20
|
+
ACCOUNT_CLOSE = "account_close"
|
|
21
|
+
ACCOUNT_FREEZE = "account_freeze"
|
|
22
|
+
ACCOUNT_UNFREEZE = "account_unfreeze"
|
|
23
|
+
|
|
24
|
+
# Transaction actions
|
|
25
|
+
TRANSACTION_CREATE = "transaction_create"
|
|
26
|
+
TRANSACTION_APPROVE = "transaction_approve"
|
|
27
|
+
TRANSACTION_REJECT = "transaction_reject"
|
|
28
|
+
TRANSACTION_REVERSE = "transaction_reverse"
|
|
29
|
+
|
|
30
|
+
# Customer actions
|
|
31
|
+
CUSTOMER_CREATE = "customer_create"
|
|
32
|
+
CUSTOMER_UPDATE = "customer_update"
|
|
33
|
+
CUSTOMER_KYC_UPDATE = "customer_kyc_update"
|
|
34
|
+
CUSTOMER_BLOCK = "customer_block"
|
|
35
|
+
|
|
36
|
+
# Card actions
|
|
37
|
+
CARD_CREATE = "card_create"
|
|
38
|
+
CARD_ACTIVATE = "card_activate"
|
|
39
|
+
CARD_BLOCK = "card_block"
|
|
40
|
+
CARD_REPLACE = "card_replace"
|
|
41
|
+
|
|
42
|
+
# Loan actions
|
|
43
|
+
LOAN_CREATE = "loan_create"
|
|
44
|
+
LOAN_APPROVE = "loan_approve"
|
|
45
|
+
LOAN_REJECT = "loan_reject"
|
|
46
|
+
LOAN_PAYMENT = "loan_payment"
|
|
47
|
+
|
|
48
|
+
# Admin actions
|
|
49
|
+
ADMIN_LOGIN = "admin_login"
|
|
50
|
+
ADMIN_LOGOUT = "admin_logout"
|
|
51
|
+
SETTINGS_CHANGE = "settings_change"
|
|
52
|
+
FEE_UPDATE = "fee_update"
|
|
53
|
+
RATE_UPDATE = "rate_update"
|
|
54
|
+
|
|
55
|
+
# Security actions
|
|
56
|
+
PASSWORD_CHANGE = "password_change"
|
|
57
|
+
PIN_CHANGE = "pin_change"
|
|
58
|
+
LIMIT_CHANGE = "limit_change"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SecurityEventType(Enum):
|
|
62
|
+
"""Types of security events"""
|
|
63
|
+
# Authentication
|
|
64
|
+
LOGIN_SUCCESS = "login_success"
|
|
65
|
+
LOGIN_FAILURE = "login_failure"
|
|
66
|
+
LOGIN_BLOCKED = "login_blocked"
|
|
67
|
+
MFA_SUCCESS = "mfa_success"
|
|
68
|
+
MFA_FAILURE = "mfa_failure"
|
|
69
|
+
SESSION_EXPIRED = "session_expired"
|
|
70
|
+
|
|
71
|
+
# Suspicious activity
|
|
72
|
+
UNUSUAL_LOCATION = "unusual_location"
|
|
73
|
+
UNUSUAL_TIME = "unusual_time"
|
|
74
|
+
UNUSUAL_AMOUNT = "unusual_amount"
|
|
75
|
+
VELOCITY_CHECK = "velocity_check"
|
|
76
|
+
FRAUD_DETECTED = "fraud_detected"
|
|
77
|
+
|
|
78
|
+
# Card events
|
|
79
|
+
CARD_STOLEN = "card_stolen"
|
|
80
|
+
CARD_CLONED = "card_cloned"
|
|
81
|
+
CARD_MISUSE = "card_misuse"
|
|
82
|
+
PIN_ATTEMPTS = "pin_attempts"
|
|
83
|
+
|
|
84
|
+
# Account events
|
|
85
|
+
ACCOUNT_TAKEOVER = "account_takeover"
|
|
86
|
+
BENEFICIARY_CHANGE = "beneficiary_change"
|
|
87
|
+
CONTACT_CHANGE = "contact_change"
|
|
88
|
+
DEVICE_CHANGE = "device_change"
|
|
89
|
+
|
|
90
|
+
# Compliance
|
|
91
|
+
AML_ALERT = "aml_alert"
|
|
92
|
+
SANCTION_CHECK = "sanction_check"
|
|
93
|
+
PEP_CHECK = "pep_check"
|
|
94
|
+
LARGE_TRANSACTION = "large_transaction"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class RiskLevel(Enum):
|
|
98
|
+
"""Risk levels for security events"""
|
|
99
|
+
LOW = "low"
|
|
100
|
+
MEDIUM = "medium"
|
|
101
|
+
HIGH = "high"
|
|
102
|
+
CRITICAL = "critical"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class EventStatus(Enum):
|
|
106
|
+
"""Status of security events"""
|
|
107
|
+
NEW = "new"
|
|
108
|
+
INVESTIGATING = "investigating"
|
|
109
|
+
CONFIRMED = "confirmed"
|
|
110
|
+
FALSE_POSITIVE = "false_positive"
|
|
111
|
+
RESOLVED = "resolved"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class AuditLog:
|
|
116
|
+
"""
|
|
117
|
+
Audit trail for all banking operations
|
|
118
|
+
"""
|
|
119
|
+
log_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
120
|
+
|
|
121
|
+
# Action details
|
|
122
|
+
action: AuditAction = AuditAction.ACCOUNT_CREATE
|
|
123
|
+
description: str = ""
|
|
124
|
+
|
|
125
|
+
# Who
|
|
126
|
+
actor_type: str = "user" # user, employee, system, api
|
|
127
|
+
actor_id: str = ""
|
|
128
|
+
actor_name: str = ""
|
|
129
|
+
actor_ip: str = ""
|
|
130
|
+
actor_device: str = ""
|
|
131
|
+
|
|
132
|
+
# What
|
|
133
|
+
entity_type: str = "" # account, transaction, customer, card, loan
|
|
134
|
+
entity_id: str = ""
|
|
135
|
+
entity_name: str = ""
|
|
136
|
+
|
|
137
|
+
# Changes
|
|
138
|
+
old_values: Dict[str, Any] = field(default_factory=dict)
|
|
139
|
+
new_values: Dict[str, Any] = field(default_factory=dict)
|
|
140
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
141
|
+
|
|
142
|
+
# When
|
|
143
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
144
|
+
|
|
145
|
+
# Result
|
|
146
|
+
success: bool = True
|
|
147
|
+
error_message: str = ""
|
|
148
|
+
|
|
149
|
+
# Compliance
|
|
150
|
+
requires_review: bool = False
|
|
151
|
+
reviewed_by: str = ""
|
|
152
|
+
reviewed_at: Optional[datetime] = None
|
|
153
|
+
|
|
154
|
+
def __post_init__(self):
|
|
155
|
+
# Generate hash for integrity
|
|
156
|
+
self._hash = self._compute_hash()
|
|
157
|
+
|
|
158
|
+
def _compute_hash(self) -> str:
|
|
159
|
+
"""Compute hash of log entry for integrity verification"""
|
|
160
|
+
data = f"{self.log_id}|{self.action.value}|{self.actor_id}|{self.entity_id}|{self.timestamp.isoformat()}"
|
|
161
|
+
return hashlib.sha256(data.encode()).hexdigest()[:16]
|
|
162
|
+
|
|
163
|
+
def verify_integrity(self) -> bool:
|
|
164
|
+
"""Verify log entry hasn't been tampered with"""
|
|
165
|
+
return self._hash == self._compute_hash()
|
|
166
|
+
|
|
167
|
+
def to_dict(self) -> dict:
|
|
168
|
+
return {
|
|
169
|
+
"log_id": self.log_id,
|
|
170
|
+
"action": self.action.value,
|
|
171
|
+
"description": self.description,
|
|
172
|
+
"actor_type": self.actor_type,
|
|
173
|
+
"actor_id": self.actor_id,
|
|
174
|
+
"actor_name": self.actor_name,
|
|
175
|
+
"actor_ip": self.actor_ip,
|
|
176
|
+
"actor_device": self.actor_device,
|
|
177
|
+
"entity_type": self.entity_type,
|
|
178
|
+
"entity_id": self.entity_id,
|
|
179
|
+
"entity_name": self.entity_name,
|
|
180
|
+
"old_values": self.old_values,
|
|
181
|
+
"new_values": self.new_values,
|
|
182
|
+
"metadata": self.metadata,
|
|
183
|
+
"timestamp": self.timestamp.isoformat(),
|
|
184
|
+
"success": self.success,
|
|
185
|
+
"error_message": self.error_message,
|
|
186
|
+
"requires_review": self.requires_review,
|
|
187
|
+
"reviewed_by": self.reviewed_by,
|
|
188
|
+
"reviewed_at": self.reviewed_at.isoformat() if self.reviewed_at else None,
|
|
189
|
+
"hash": self._hash,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class SecurityEvent:
|
|
195
|
+
"""
|
|
196
|
+
Security event / alert
|
|
197
|
+
"""
|
|
198
|
+
event_id: str = field(default_factory=lambda: f"SEC{uuid.uuid4().hex[:10].upper()}")
|
|
199
|
+
|
|
200
|
+
# Event details
|
|
201
|
+
event_type: SecurityEventType = SecurityEventType.LOGIN_FAILURE
|
|
202
|
+
risk_level: RiskLevel = RiskLevel.LOW
|
|
203
|
+
status: EventStatus = EventStatus.NEW
|
|
204
|
+
|
|
205
|
+
# Description
|
|
206
|
+
title: str = ""
|
|
207
|
+
description: str = ""
|
|
208
|
+
recommendation: str = ""
|
|
209
|
+
|
|
210
|
+
# Who is affected
|
|
211
|
+
customer_id: Optional[str] = None
|
|
212
|
+
account_iban: Optional[str] = None
|
|
213
|
+
card_id: Optional[str] = None
|
|
214
|
+
employee_id: Optional[str] = None
|
|
215
|
+
|
|
216
|
+
# Context
|
|
217
|
+
ip_address: str = ""
|
|
218
|
+
user_agent: str = ""
|
|
219
|
+
device_id: str = ""
|
|
220
|
+
location: str = ""
|
|
221
|
+
country: str = ""
|
|
222
|
+
|
|
223
|
+
# Transaction related
|
|
224
|
+
transaction_id: Optional[str] = None
|
|
225
|
+
amount: Optional[Decimal] = None
|
|
226
|
+
|
|
227
|
+
# Timing
|
|
228
|
+
detected_at: datetime = field(default_factory=datetime.now)
|
|
229
|
+
resolved_at: Optional[datetime] = None
|
|
230
|
+
|
|
231
|
+
# Investigation
|
|
232
|
+
assigned_to: str = ""
|
|
233
|
+
notes: str = ""
|
|
234
|
+
resolution: str = ""
|
|
235
|
+
|
|
236
|
+
# Related events
|
|
237
|
+
related_events: list = field(default_factory=list)
|
|
238
|
+
|
|
239
|
+
# Metadata
|
|
240
|
+
raw_data: Dict[str, Any] = field(default_factory=dict)
|
|
241
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
242
|
+
|
|
243
|
+
def escalate(self, new_level: RiskLevel, reason: str):
|
|
244
|
+
"""Escalate event to higher risk level"""
|
|
245
|
+
self.risk_level = new_level
|
|
246
|
+
self.notes += f"\n[Escalated] {datetime.now().isoformat()}: {reason}"
|
|
247
|
+
|
|
248
|
+
def resolve(self, resolution: str, resolved_by: str):
|
|
249
|
+
"""Mark event as resolved"""
|
|
250
|
+
self.status = EventStatus.RESOLVED
|
|
251
|
+
self.resolution = resolution
|
|
252
|
+
self.resolved_at = datetime.now()
|
|
253
|
+
self.notes += f"\n[Resolved] {datetime.now().isoformat()} by {resolved_by}: {resolution}"
|
|
254
|
+
|
|
255
|
+
def to_dict(self) -> dict:
|
|
256
|
+
return {
|
|
257
|
+
"event_id": self.event_id,
|
|
258
|
+
"event_type": self.event_type.value,
|
|
259
|
+
"risk_level": self.risk_level.value,
|
|
260
|
+
"status": self.status.value,
|
|
261
|
+
"title": self.title,
|
|
262
|
+
"description": self.description,
|
|
263
|
+
"recommendation": self.recommendation,
|
|
264
|
+
"customer_id": self.customer_id,
|
|
265
|
+
"account_iban": self.account_iban,
|
|
266
|
+
"card_id": self.card_id,
|
|
267
|
+
"employee_id": self.employee_id,
|
|
268
|
+
"ip_address": self.ip_address,
|
|
269
|
+
"user_agent": self.user_agent,
|
|
270
|
+
"device_id": self.device_id,
|
|
271
|
+
"location": self.location,
|
|
272
|
+
"country": self.country,
|
|
273
|
+
"transaction_id": self.transaction_id,
|
|
274
|
+
"amount": str(self.amount) if self.amount else None,
|
|
275
|
+
"detected_at": self.detected_at.isoformat(),
|
|
276
|
+
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None,
|
|
277
|
+
"assigned_to": self.assigned_to,
|
|
278
|
+
"notes": self.notes,
|
|
279
|
+
"resolution": self.resolution,
|
|
280
|
+
"related_events": self.related_events,
|
|
281
|
+
"created_at": self.created_at.isoformat(),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class ComplianceCheck:
|
|
287
|
+
"""
|
|
288
|
+
Compliance verification record (KYC, AML, etc.)
|
|
289
|
+
"""
|
|
290
|
+
check_id: str = field(default_factory=lambda: f"CHK{uuid.uuid4().hex[:10].upper()}")
|
|
291
|
+
|
|
292
|
+
# Check type
|
|
293
|
+
check_type: str = "" # kyc, aml, sanction, pep, fatca
|
|
294
|
+
customer_id: str = ""
|
|
295
|
+
|
|
296
|
+
# Result
|
|
297
|
+
passed: bool = True
|
|
298
|
+
score: int = 0 # Risk score 0-100
|
|
299
|
+
findings: list = field(default_factory=list)
|
|
300
|
+
|
|
301
|
+
# Documents
|
|
302
|
+
documents_verified: list = field(default_factory=list)
|
|
303
|
+
documents_missing: list = field(default_factory=list)
|
|
304
|
+
|
|
305
|
+
# External checks
|
|
306
|
+
external_provider: str = ""
|
|
307
|
+
external_reference: str = ""
|
|
308
|
+
external_response: Dict[str, Any] = field(default_factory=dict)
|
|
309
|
+
|
|
310
|
+
# Status
|
|
311
|
+
status: str = "pending" # pending, passed, failed, review
|
|
312
|
+
requires_review: bool = False
|
|
313
|
+
reviewed_by: str = ""
|
|
314
|
+
reviewed_at: Optional[datetime] = None
|
|
315
|
+
review_notes: str = ""
|
|
316
|
+
|
|
317
|
+
# Validity
|
|
318
|
+
valid_until: Optional[datetime] = None
|
|
319
|
+
next_review: Optional[datetime] = None
|
|
320
|
+
|
|
321
|
+
# Metadata
|
|
322
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
323
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
324
|
+
|
|
325
|
+
def to_dict(self) -> dict:
|
|
326
|
+
return {
|
|
327
|
+
"check_id": self.check_id,
|
|
328
|
+
"check_type": self.check_type,
|
|
329
|
+
"customer_id": self.customer_id,
|
|
330
|
+
"passed": self.passed,
|
|
331
|
+
"score": self.score,
|
|
332
|
+
"findings": self.findings,
|
|
333
|
+
"documents_verified": self.documents_verified,
|
|
334
|
+
"documents_missing": self.documents_missing,
|
|
335
|
+
"external_provider": self.external_provider,
|
|
336
|
+
"external_reference": self.external_reference,
|
|
337
|
+
"status": self.status,
|
|
338
|
+
"requires_review": self.requires_review,
|
|
339
|
+
"reviewed_by": self.reviewed_by,
|
|
340
|
+
"reviewed_at": self.reviewed_at.isoformat() if self.reviewed_at else None,
|
|
341
|
+
"review_notes": self.review_notes,
|
|
342
|
+
"valid_until": self.valid_until.isoformat() if self.valid_until else None,
|
|
343
|
+
"next_review": self.next_review.isoformat() if self.next_review else None,
|
|
344
|
+
"created_at": self.created_at.isoformat(),
|
|
345
|
+
"updated_at": self.updated_at.isoformat(),
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class AuditLogger:
|
|
350
|
+
"""
|
|
351
|
+
Helper class to create audit logs easily
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
def __init__(self):
|
|
355
|
+
self.logs: list[AuditLog] = []
|
|
356
|
+
|
|
357
|
+
def log(
|
|
358
|
+
self,
|
|
359
|
+
action: AuditAction,
|
|
360
|
+
actor_id: str,
|
|
361
|
+
entity_type: str,
|
|
362
|
+
entity_id: str,
|
|
363
|
+
description: str = "",
|
|
364
|
+
old_values: dict = None,
|
|
365
|
+
new_values: dict = None,
|
|
366
|
+
**kwargs
|
|
367
|
+
) -> AuditLog:
|
|
368
|
+
"""Create and store an audit log entry"""
|
|
369
|
+
log = AuditLog(
|
|
370
|
+
action=action,
|
|
371
|
+
description=description or f"{action.value} on {entity_type} {entity_id}",
|
|
372
|
+
actor_id=actor_id,
|
|
373
|
+
entity_type=entity_type,
|
|
374
|
+
entity_id=entity_id,
|
|
375
|
+
old_values=old_values or {},
|
|
376
|
+
new_values=new_values or {},
|
|
377
|
+
**kwargs
|
|
378
|
+
)
|
|
379
|
+
self.logs.append(log)
|
|
380
|
+
return log
|
|
381
|
+
|
|
382
|
+
def get_logs(
|
|
383
|
+
self,
|
|
384
|
+
entity_id: str = None,
|
|
385
|
+
actor_id: str = None,
|
|
386
|
+
action: AuditAction = None,
|
|
387
|
+
from_date: datetime = None,
|
|
388
|
+
to_date: datetime = None
|
|
389
|
+
) -> list[AuditLog]:
|
|
390
|
+
"""Query audit logs with filters"""
|
|
391
|
+
results = self.logs
|
|
392
|
+
|
|
393
|
+
if entity_id:
|
|
394
|
+
results = [l for l in results if l.entity_id == entity_id]
|
|
395
|
+
if actor_id:
|
|
396
|
+
results = [l for l in results if l.actor_id == actor_id]
|
|
397
|
+
if action:
|
|
398
|
+
results = [l for l in results if l.action == action]
|
|
399
|
+
if from_date:
|
|
400
|
+
results = [l for l in results if l.timestamp >= from_date]
|
|
401
|
+
if to_date:
|
|
402
|
+
results = [l for l in results if l.timestamp <= to_date]
|
|
403
|
+
|
|
404
|
+
return sorted(results, key=lambda x: x.timestamp, reverse=True)
|