Add an 80s-style green-on-black bank terminal shell ("banking") using
charmbracelet/bubbletea for full-screen TUI rendering over SSH.
Screens: login, main menu, account summary, account detail with
transactions, wire transfer wizard (6-step form capturing routing
number, destination, beneficiary, amount, memo, auth code), transaction
history with pagination, secure messages with breadcrumb content (fake
internal IPs, vault codes), change PIN, and hidden admin access (99)
that locks after 3 failed attempts with COBOL-style error output.
All key actions (login, navigation, wire transfers, admin attempts) are
logged to the session store. Wire transfer data is the honeypot gold.
Configurable via [shell.banking] in TOML: bank_name, terminal_id, region.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
259 lines
8.3 KiB
Go
259 lines
8.3 KiB
Go
package banking
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Account types.
|
|
const (
|
|
AcctChecking = "CHECKING"
|
|
AcctSavings = "SAVINGS"
|
|
AcctMoneyMarket = "MONEY MARKET"
|
|
AcctCertDeposit = "CERT OF DEPOSIT"
|
|
)
|
|
|
|
// Account represents a fake bank account.
|
|
type Account struct {
|
|
Number string
|
|
Type string
|
|
Balance int64 // cents
|
|
}
|
|
|
|
// Transaction represents a fake bank transaction.
|
|
type Transaction struct {
|
|
Date string
|
|
Description string
|
|
Amount int64 // cents (negative for debits)
|
|
Balance int64 // running balance in cents
|
|
}
|
|
|
|
// SecureMessage represents a fake internal message.
|
|
type SecureMessage struct {
|
|
ID int
|
|
Date string
|
|
From string
|
|
Subj string
|
|
Body string
|
|
Unread bool
|
|
}
|
|
|
|
// WireTransfer captures data entered during the wire transfer wizard.
|
|
type WireTransfer struct {
|
|
RoutingNumber string
|
|
DestAccount string
|
|
Beneficiary string
|
|
BankName string
|
|
Amount string
|
|
Memo string
|
|
AuthCode string
|
|
}
|
|
|
|
// bankState holds all fake data for a session.
|
|
type bankState struct {
|
|
Accounts []Account
|
|
Transactions map[string][]Transaction // keyed by account number
|
|
Messages []SecureMessage
|
|
Transfers []WireTransfer
|
|
}
|
|
|
|
func newBankState() *bankState {
|
|
now := time.Now()
|
|
|
|
accounts := []Account{
|
|
{Number: "****4821", Type: AcctChecking, Balance: 4738291},
|
|
{Number: "****7203", Type: AcctSavings, Balance: 18254100},
|
|
{Number: "****9915", Type: AcctMoneyMarket, Balance: 52387450},
|
|
{Number: "****1102", Type: AcctCertDeposit, Balance: 25000000},
|
|
}
|
|
|
|
transactions := make(map[string][]Transaction)
|
|
transactions["****4821"] = generateCheckingTxns(now, accounts[0].Balance)
|
|
transactions["****7203"] = generateSavingsTxns(now, accounts[1].Balance)
|
|
transactions["****9915"] = generateMoneyMarketTxns(now, accounts[2].Balance)
|
|
transactions["****1102"] = generateCDTxns(now, accounts[3].Balance)
|
|
|
|
messages := []SecureMessage{
|
|
{
|
|
ID: 1,
|
|
Date: now.Add(-2 * 24 * time.Hour).Format("01/02/2006"),
|
|
From: "SYSTEM ADMINISTRATOR",
|
|
Subj: "SCHEDULED MAINTENANCE WINDOW",
|
|
Unread: true,
|
|
Body: fmt.Sprintf(`FROM: SYSTEM ADMINISTRATOR <sysadmin@internal.securebank.local>
|
|
DATE: %s
|
|
RE: SCHEDULED MAINTENANCE WINDOW
|
|
|
|
ALL TERMINALS WILL BE OFFLINE FOR MAINTENANCE:
|
|
DATE: %s
|
|
TIME: 02:00 - 04:00 EST
|
|
AFFECTED: ALL REGIONS
|
|
|
|
DURING THIS WINDOW, THE FOLLOWING SYSTEMS WILL BE UNAVAILABLE:
|
|
- WIRE TRANSFER PROCESSING (10.48.2.100:8443)
|
|
- ACCOUNT MANAGEMENT (10.48.2.101:8443)
|
|
- ACH BATCH PROCESSOR (10.48.2.105:9090)
|
|
|
|
PLEASE ENSURE ALL PENDING TRANSACTIONS ARE SUBMITTED BEFORE 01:30 EST.
|
|
|
|
CONTACT: HELPDESK EXT 4400 OR ops-support@internal.securebank.local`,
|
|
now.Add(-2*24*time.Hour).Format("01/02/2006 15:04"),
|
|
now.Add(5*24*time.Hour).Format("01/02/2006")),
|
|
},
|
|
{
|
|
ID: 2,
|
|
Date: now.Add(-5 * 24 * time.Hour).Format("01/02/2006"),
|
|
From: "COMPLIANCE DEPT",
|
|
Subj: "QUARTERLY AUDIT REMINDER",
|
|
Unread: true,
|
|
Body: `FROM: COMPLIANCE DEPT <compliance@internal.securebank.local>
|
|
RE: QUARTERLY AUDIT REMINDER
|
|
|
|
ALL BRANCH MANAGERS:
|
|
|
|
THE Q4 COMPLIANCE AUDIT IS SCHEDULED FOR NEXT WEEK.
|
|
PLEASE ENSURE THE FOLLOWING ARE CURRENT:
|
|
|
|
1. TRANSACTION LOGS EXPORTED TO \\FILESERV01\AUDIT\Q4
|
|
2. VAULT ACCESS CODES ROTATED (LAST ROTATION: SEE VAULT-MGMT PORTAL)
|
|
3. EMPLOYEE ACCESS REVIEWS COMPLETED IN IAM PORTAL (https://iam.internal:8443)
|
|
|
|
NOTE: DEFAULT CREDENTIALS FOR THE AUDIT PORTAL HAVE BEEN RESET.
|
|
NEW CREDENTIALS DISTRIBUTED VIA SECURE COURIER.
|
|
REFERENCE: AUDIT-2024-Q4-0847
|
|
|
|
VAULT MASTER CODE HINT: FIRST 4 OF ROUTING + BRANCH ZIP (STANDARD FORMAT)`,
|
|
},
|
|
{
|
|
ID: 3,
|
|
Date: now.Add(-8 * 24 * time.Hour).Format("01/02/2006"),
|
|
From: "IT SECURITY",
|
|
Subj: "PASSWORD POLICY UPDATE",
|
|
Unread: false,
|
|
Body: `FROM: IT SECURITY <itsec@internal.securebank.local>
|
|
RE: PASSWORD POLICY UPDATE - EFFECTIVE IMMEDIATELY
|
|
|
|
ALL STAFF:
|
|
|
|
PER FEDERAL BANKING REGULATION 12 CFR 748, THE FOLLOWING
|
|
PASSWORD POLICY IS NOW IN EFFECT:
|
|
|
|
- MINIMUM 12 CHARACTERS
|
|
- MUST CONTAIN UPPERCASE, LOWERCASE, NUMBER, SPECIAL CHAR
|
|
- 90-DAY ROTATION CYCLE
|
|
- NO REUSE OF LAST 24 PASSWORDS
|
|
|
|
LEGACY SYSTEM ACCOUNTS (MAINFRAME, AS/400) ARE EXEMPT UNTIL
|
|
MIGRATION IS COMPLETE. CURRENT LEGACY ACCESS:
|
|
MAINFRAME: telnet://10.48.1.50:23 (CICS REGION PROD1)
|
|
AS/400: tn5250://10.48.1.55 (SUBSYSTEM QINTER)
|
|
|
|
SERVICE ACCOUNT PASSWORDS ARE MANAGED VIA CYBERARK:
|
|
https://pam.internal.securebank.local:8443
|
|
|
|
TICKET: SEC-2024-1847`,
|
|
},
|
|
{
|
|
ID: 4,
|
|
Date: now.Add(-12 * 24 * time.Hour).Format("01/02/2006"),
|
|
From: "WIRE OPERATIONS",
|
|
Subj: "FEDWIRE CUTOFF TIME CHANGE",
|
|
Unread: false,
|
|
Body: `FROM: WIRE OPERATIONS <wireops@internal.securebank.local>
|
|
RE: FEDWIRE CUTOFF TIME CHANGE
|
|
|
|
EFFECTIVE NEXT MONDAY, FEDWIRE CUTOFF TIMES ARE:
|
|
DOMESTIC WIRES: 16:30 EST (WAS 17:00)
|
|
INTERNATIONAL WIRES: 14:00 EST (NO CHANGE)
|
|
BOOK TRANSFERS: 17:30 EST (NO CHANGE)
|
|
|
|
WIRES SUBMITTED AFTER CUTOFF WILL BE QUEUED FOR NEXT
|
|
BUSINESS DAY PROCESSING.
|
|
|
|
FOR EMERGENCY SAME-DAY PROCESSING AFTER CUTOFF:
|
|
CONTACT WIRE ROOM: EXT 4450
|
|
AUTH CODE REQUIRED (OBTAIN FROM BRANCH MANAGER)
|
|
APPROVAL CHAIN: OPS-MGR -> VP-WIRE -> SVP-TREASURY
|
|
|
|
CORRESPONDENT BANK CONTACTS:
|
|
JPMORGAN: wire.ops@jpmc.com / 212-555-0147
|
|
CITI: fedwire@citi.com / 212-555-0283`,
|
|
},
|
|
}
|
|
|
|
return &bankState{
|
|
Accounts: accounts,
|
|
Transactions: transactions,
|
|
Messages: messages,
|
|
}
|
|
}
|
|
|
|
func generateCheckingTxns(now time.Time, endBalance int64) []Transaction {
|
|
txns := []Transaction{
|
|
{Description: "ACH DEPOSIT - PAYROLL", Amount: 485000},
|
|
{Description: "CHECK #1847", Amount: -125000},
|
|
{Description: "POS DEBIT - WHOLE FOODS #1284", Amount: -18743},
|
|
{Description: "ATM WITHDRAWAL - MAIN ST BRANCH", Amount: -40000},
|
|
{Description: "ACH DEBIT - MORTGAGE PMT", Amount: -215000},
|
|
{Description: "WIRE TRANSFER IN - REF#8847201", Amount: 1250000},
|
|
{Description: "POS DEBIT - SHELL OIL #4492", Amount: -6821},
|
|
{Description: "ACH DEPOSIT - PAYROLL", Amount: 485000},
|
|
{Description: "CHECK #1848", Amount: -75000},
|
|
{Description: "ONLINE TRANSFER TO SAVINGS", Amount: -100000},
|
|
{Description: "POS DEBIT - AMAZON.COM", Amount: -14599},
|
|
{Description: "ACH DEBIT - ELECTRIC COMPANY", Amount: -18742},
|
|
{Description: "ATM WITHDRAWAL - PARK AVE BRANCH", Amount: -20000},
|
|
{Description: "WIRE TRANSFER OUT - REF#9014882", Amount: -500000},
|
|
{Description: "POS DEBIT - COSTCO #0441", Amount: -28734},
|
|
{Description: "ACH DEPOSIT - TAX REFUND", Amount: 342100},
|
|
}
|
|
return populateTransactions(txns, now, endBalance)
|
|
}
|
|
|
|
func generateSavingsTxns(now time.Time, endBalance int64) []Transaction {
|
|
txns := []Transaction{
|
|
{Description: "INTEREST PAYMENT", Amount: 4521},
|
|
{Description: "ONLINE TRANSFER FROM CHECKING", Amount: 100000},
|
|
{Description: "INTEREST PAYMENT", Amount: 4633},
|
|
{Description: "ACH DEPOSIT - DIVIDEND PMT", Amount: 125000},
|
|
{Description: "ONLINE TRANSFER FROM CHECKING", Amount: 200000},
|
|
{Description: "INTEREST PAYMENT", Amount: 4748},
|
|
{Description: "WITHDRAWAL - TRANSFER TO MM", Amount: -500000},
|
|
{Description: "INTEREST PAYMENT", Amount: 4812},
|
|
}
|
|
return populateTransactions(txns, now, endBalance)
|
|
}
|
|
|
|
func generateMoneyMarketTxns(now time.Time, endBalance int64) []Transaction {
|
|
txns := []Transaction{
|
|
{Description: "INTEREST PAYMENT - TIER 3 RATE", Amount: 21847},
|
|
{Description: "DEPOSIT - TRANSFER FROM SAVINGS", Amount: 500000},
|
|
{Description: "INTEREST PAYMENT - TIER 3 RATE", Amount: 22105},
|
|
{Description: "WITHDRAWAL - WIRE TRANSFER", Amount: -1000000},
|
|
{Description: "DEPOSIT - ACH TRANSFER", Amount: 750000},
|
|
{Description: "INTEREST PAYMENT - TIER 3 RATE", Amount: 22394},
|
|
}
|
|
return populateTransactions(txns, now, endBalance)
|
|
}
|
|
|
|
func generateCDTxns(now time.Time, endBalance int64) []Transaction {
|
|
txns := []Transaction{
|
|
{Description: "CERTIFICATE OPENED - 12MO TERM", Amount: 25000000},
|
|
{Description: "INTEREST ACCRUAL", Amount: 10417},
|
|
{Description: "INTEREST ACCRUAL", Amount: 10417},
|
|
{Description: "INTEREST ACCRUAL", Amount: 10417},
|
|
}
|
|
return populateTransactions(txns, now, endBalance)
|
|
}
|
|
|
|
func populateTransactions(txns []Transaction, now time.Time, endBalance int64) []Transaction {
|
|
// Work backwards from end balance to assign dates and running balances.
|
|
bal := endBalance
|
|
for i := len(txns) - 1; i >= 0; i-- {
|
|
txns[i].Balance = bal
|
|
txns[i].Date = now.Add(time.Duration(-(len(txns) - i)) * 3 * 24 * time.Hour).Format("01/02/2006")
|
|
bal -= txns[i].Amount
|
|
}
|
|
return txns
|
|
}
|