Files
claudetools/clients/glaztech/reports/2026-06-03-website-security-assessment.md
Mike Swanson 64b2d9e668 sync: auto-sync from GURU-5070 at 2026-06-04 07:07:43
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-04 07:07:43
2026-06-04 07:07:48 -07:00

231 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Glaz-Tech Industries — Website Security Assessment
**Classification:** CONFIDENTIAL — Security
**Date:** 2026-06-03
**Assessor:** Arizona Computer Guru (Mike Swanson)
**Target:** Glaztech customer/e-commerce web application — server `WWW` (192.168.8.72, public 65.113.52.88), site `glaztech_new` at `D:\web\glaztech_4`, SQL backend `192.168.8.62,3436`
**Method:** Authorized read-only assessment via GuruRMM (config/registry inspection, source-code review of the on-server VB.NET source, and read-only/aggregate DB inspection). **No cardholder data and no passwords were retrieved** — sensitive columns were classified by aggregate only.
> Companion report: `2026-06-03-pci-cardholder-data-finding.md` (cardholder-data storage detail).
---
## Overall Risk: CRITICAL
The site stores cardholder data (PAN + CVV) and **all user passwords in plaintext**, contains SQL injection and reflected XSS, and runs on a server that doubles as a developer workstation with extensive remote-access and end-of-life software. Multiple findings are independently sufficient to cause a reportable breach.
| # | Finding | Severity |
|---|---|---|
| C0 | **Internet-facing website connects to SQL Server as `sysadmin`** — a SQLi gives full control of the entire shared instance | Critical (top) |
| C1 | Plaintext PAN + stored CVV in DB | Critical |
| C2 | All user passwords stored in plaintext; passwords emailed in cleartext | Critical |
| C3 | SQL injection via fake `quo()` escaper (incl. payment pages) | Critical |
| C4 | Reflected XSS in `gt_errorpage.aspx` | High |
| H1 | Production payment server is also a dev workstation (VS, SDKs, build tools) | High |
| H2 | Remote-access sprawl incl. end-of-life RealVNC 4.2.8 + stale ScreenConnect v6 | High |
| H3 | `debug="true"` + `customErrors=Off` + exceptions echoed to users | High |
| H4 | Server accepts TLS 1.0/1.1 on the listener | High |
| H5 | No cookie Secure/HttpOnly hardening, no MFA, no lockout, session-fixation risk | High |
| H6 | Single shared SQL login with full card-column read; creds in `Web.config` | High |
| M1 | Outdated/unused third-party components; SHA1 machineKey; source on prod | Medium |
---
## Current State Verified — 2026-06-03 (exposure is ACTIVE and ONGOING)
Re-checked the live system after a report that card processing had moved to a different provider ("Payrilla"):
- **The website is still on CyberSource/PNC.** There is **no Payrilla integration** anywhere in the site code or config; the live payment pages remain `online-payment-pnc.aspx` / `quick-pay-pnc.aspx` → CyberSource (`api.cybersource.com`).
- **Card processing is live via PNC today** — `web_payment_header` shows `CC-WebPayment-PNC` approvals/errors on 2026-06-03 (e.g. 14:47 Approved, 15:07 Error) plus E-Check.
- **Plaintext cards are still being written to the local databases every day** — `cc_file` last write: Tucson **2026-06-03 14:15** (8 writes/60d), Phoenix **2026-06-03 10:19** (14 writes/60d). This is ongoing accumulation, not historical data.
- *(Anomaly: `cc_file` returns "Invalid object name" in the `corp` DB though it existed there earlier the same day — needs a second look; the per-office DBs are unaffected and still writing.)*
- **Where is "Payrilla/Paya," then?** Not in the website (CyberSource) and not in Sage 100 — Sage's native CC module is **disabled** (`CreditCardEnable = N`, 0 stored cards). So the new processor is **not visible in either system we can reach**; if it is in use it is a **separate channel** (e.g. a standalone Paya/Payrilla virtual terminal or a different portal) that does **not** cover the website. This is the likely source of the "we moved to Payrilla for everything" belief — it may be true for some manual/standalone processing, but it is **not** true for the customer website, which is the system storing plaintext cards.
**Implication:** any move to Payrilla has **not** been applied to this website. The plaintext PAN + CVV exposure in this report is fully active and **growing daily**; switching processors provides no protection until the website is actually re-pointed to a tokenized store and the stored data is purged. The correct remediation path is to **complete a real migration to the new processor's secure token vault** (store a token, never the PAN), stop writing `cc_file`/`cof_payments_header`, purge the existing plaintext cards + CVV, and then decommission CyberSource. Treat this as a live, present risk.
---
## C0 (TOP CRITICAL) — The internet-facing website connects to SQL Server as `sysadmin`
The single most dangerous fact in this engagement (verified 2026-06-03). The public website logs into the database with login **`tom`**, which is a **SQL Server `sysadmin`** (also `securityadmin`, `dbcreator`, `db_owner`) — i.e. **complete control of the entire instance** at `192.168.8.62,3436` (SQL host `GTI-INV-SQL`). `tom` is a **named SQL login** (SQL authentication, created 2018) that is a *member of the `sysadmin` role***not** the built-in `sa` and **not** a constrained service account — and its **password is stored in the website's `Web.config`** on the internet-facing server, so anyone who can read that file (via the SQLi, the dev tooling on the box, or a file-disclosure bug) obtains a sysadmin credential outright. That instance is **shared with the internal GTIware PSA** and hosts **46 databases**: every office's production DB (`glaz_prod*`), all `*_archive` databases (years of history), the PDF stores, TimeForce (`qqest`), `gti_samsara`, and the system DBs (`master`, `msdb`).
Because a SQL injection runs **as the connecting login**, the website's SQLi flaw executes with `sysadmin` rights over that whole instance. Cross-database reach is confirmed live: from a single injectable page on one office's connection we read other offices' card tables (`glaz_prod_phx.dbo.cc_file` = 141, `glaz_prod_den` = 190, `glaz_prod_elp` = 179).
### Attack chain — low effort, catastrophic result
1. **Guess one website login** (username = the customer account number, enumerable; passwords plaintext, as short as 3 chars; **no lockout/rate-limiting**).
2. **Hit an injectable page** (payment pages build SQL via the non-escaping `quo()` helper).
3. **Inject as `sysadmin`** → total control of the SQL server.
### What an attacker can do as `sysadmin` (spelled out)
**Steal everything:** dump **all offices' stored cards (full PAN + CVV)** from `cc_file` + the entire `cof_payments_header` history (tens of thousands of card numbers); dump **~9,000+ plaintext passwords** (`web_security`); dump every customer/invoice/order/balance and the multi-year **archive** DBs; read TimeForce/payroll (`qqest`) and anything else on the instance.
**Destroy everything:** `DROP DATABASE`/`DROP TABLE`/`DELETE`/`TRUNCATE` across all 46 databases — wipe live production for **every office** and the archives; or encrypt/drop and ransom (backups are also reachable, so recovery may be impossible).
**Manipulate / commit fraud:** alter invoices, balances, prices, payment records; change account access levels (grant themselves admin); insert fraudulent transactions; falsify or delete audit history.
**Take over the server + network:** enable `xp_cmdshell` to run **OS commands on the SQL server (192.168.8.62)** as the SQL service account — install malware/ransomware, create OS accounts, exfiltrate files, **disable backups and AV**; harvest credentials and **pivot into the wider GTI/Glaztech network** (linked servers, shares, the service account's domain rights); plant persistent SQL backdoors and erase SQL audit trails.
**Bottom line: one guessed customer password is one SQL-injection away from full theft, destruction, or ransom of the entire GTIware database server — and a foothold into the rest of the network.**
### Required remediation (do this FIRST)
1. **Stop the website connecting as `sysadmin` immediately.** Give the website a dedicated **least-privilege** SQL login scoped to only the specific tables/views/procs it needs, with **no access to the GTIware databases** (`cc_file`, `web_security`, other offices, archives). Revoke `sysadmin`/`securityadmin`/`dbcreator`.
2. **Separate the website's data from GTIware** — the internet-facing app must not share a SQL instance with the internal PSA's cardholder data. Either move the website to its own database/instance, **or** strictly partition permissions so the public web login **cannot see or reach** the GTIware databases.
3. **Stop storing cardholder data on-prem at all** (best fix): either **call the processor's API directly** (processor-hosted fields / vault — no card data lands on Glaztech systems) or, at minimum, **tokenize the entire process** (store a processor token, never the PAN). **CVV must never be stored — period (PCI Req 3.2).** Any cardholder data that must be at rest has to be **encrypted**.
4. Then: fix the SQL injection (parameterize, remove `quo()`), add login lockout/rate-limiting, hash passwords.
---
## Critical Findings
### C1 — Plaintext PAN and stored CVV (see companion PCI report)
`cc_file` (~780 saved cards) and `cof_payments_header` (tens of thousands of rows; e.g. Phoenix 14,496 / 11,794 plaintext) store full card numbers unencrypted, and `cc_file.cc_code` retains CVV (PCI Req 3.2 — prohibited). Detail and remediation in the PCI report.
### C2 — All passwords stored in plaintext; cleartext password email
- **Customer portal:** auth stored proc `get_web_accesslevel` compares `web_security.web_password = @passwd` **with no hashing**. `web_security` holds **~9,000+ plaintext passwords** (corp 6,017 + tuc 3,012 + other offices), 0 hash-like values, lengths 319.
- **Employees:** `emp/employee-login.aspx` "forgot password" verifies last name + email, then **emails the user their existing plaintext password** (`"The password to your employee profile is: " + pword`) — only possible with reversible/plaintext storage.
- **Impact:** any DB read (or the existing SQLi) exposes every customer/employee credential in the clear; password reuse means broad downstream compromise. Weak "lastname + email" knowledge check gates the password email.
- **Fix:** store only salted password hashes (PBKDF2/bcrypt/Argon2); never email passwords — implement a reset-token flow; force a global password reset after remediation.
### C3 — SQL injection via non-escaping `quo()` helper
```vb
Function quo(stext) As String
Return "'" + stext + "'" ' wraps in quotes but does NOT escape embedded quotes
End Function
```
- Used to build **concatenated dynamic SQL** in multiple pages including payment flows (`ach.aspx.vb`, `quick-pay-ach.aspx.vb`, `quick-pay-pnc.aspx.vb`, `quick-pay.aspx.vb`, `order-detail*`). Any input containing `'` breaks out of the string → injection.
- Codebase posture is *mixed*: 948 properly parameterized calls vs. **59 concatenated SQL statements** (~10 joining user input). The login path itself is parameterized (sproc) and **not** injectable; the risk is the concatenated set.
- **Fix:** replace all concatenation with parameterized commands / stored procedures; delete `quo()`. Prioritize payment pages.
### C4 — Reflected XSS in `gt_errorpage.aspx`
- `smessage = Request.QueryString("errmsg")` (line 20) → `lblerr.Text = smessage` (line 48). `Label.Text` is **not** HTML-encoded, and the app redirects many exceptions to `gt_errorpage.aspx?errmsg=<msg>` (often containing raw `ex.Message`). An attacker-supplied `errmsg=<script>…</script>` executes in the victim's browser.
- **Fix:** HTML-encode (`Server.HtmlEncode`) before output; stop placing exception text in URLs; show generic errors to users and log details server-side.
---
## Attack Path — A Single Guessed Login → the Entire Card Database
Chaining the findings into the realistic worst case, with difficulty ratings.
**Step 1 — Obtain a customer login (LOW).** Username = the customer **account number** (enumerable, not secret). Passwords are plaintext, **as short as 3 characters**, no complexity rules, and there is **no account lockout or rate-limiting** — unlimited guessing / credential-stuffing.
**Step 2 — Normal UI (masked display).** Payment pages *display* cards masked to last-4 (`xxxx-xxxx-xxxx-1234`), so a point-and-click attacker sees last-4 + expiry + cardholder/billing data and can transact on saved cards. Note: the read proc `get_cc_data` is `SELECT * FROM cc_file WHERE acct_no=@acctno` — it returns the **full PAN and CVV to the application server**; only the *display* is masked, and the `@acctno` parameter makes it an IDOR-shaped full-card read. Any endpoint returning that proc's output unmasked (or the SQLi below) yields full numbers.
**Step 3 — SQL injection (FULL exposure).** The post-login payment pages (`quick-pay`, `ach`, `quick-pay-pnc`) build SQL with the non-escaping `quo()` helper and require only a valid session. A logged-in attacker can UNION-inject `SELECT cc_number, cc_code FROM cc_file` and exfiltrate **every stored full card number AND CVV** for the office — directly, because the data is **plaintext** (no encryption/key to defeat). UI masking is irrelevant at this layer.
| Goal | Difficulty |
|---|---|
| Obtain a valid login | **Low** (no lockout, guessable username, 3-char plaintext passwords) |
| See last-4 / transact via UI | **Low** |
| Exfiltrate ALL full PAN + CVV | **LowModerate** (one login + standard SQLi; plaintext data) |
**There is no defense-in-depth** — every compensating control (lockout, password hashing, PAN encryption, parameterized queries) is absent, so the first failure is the last failure. Highest-leverage breakers: login lockout/rate-limiting, parameterize the payment-page SQL (remove `quo()`), purge CVV + tokenize/encrypt PAN.
---
## Why the Cards Are Stored, and Where They Flow
**Business purpose — card-on-file invoice auto-pay.** Cards are stored (with an `activate` flag on `cc_file`) so the business can automatically charge customers' open invoices. The proc `i_get_cc_on_file_invoices` joins `invoice` × `cc_file` for active cards with an outstanding, delivered balance; `gt_auto_process_2020.dll` / `glaztech_utilities_2020.dll` are the engine that reads the stored card and bills it (currently via CyberSource). **This is GTIware (the internal PSA), staff-operated — NOT the website.** The website has **zero** card-handling code (no `cc_file`/`save_cc_data`/`gt_auto_process` references in its `.aspx`/`.vb`); its quick-pay pages even disclaim saving cards. Saved-card rows are stamped with **staff** usernames (Victoria, Bryce, Diana) and notes like "run card when requested." The large `cof_payments_header` history (e.g. 14,496 rows in Phoenix) is years of these GTIware charges.
**Where the full PAN is used.** Only five DB objects reference the full `cc_number`: `save_cc_data`/`save_cc_data1`/`save_cc_data2` (writes) and the `is_cc_active`/`is_cc_on_file` functions. However, **`get_cc_data` is `SELECT *`**, so it also returns the full PAN + CVV whenever a saved card is read for charging — the full number crosses to the app server on every card-on-file charge; the UI only masks the *display*.
**Containment — does NOT spread to other systems.** The Sage 100 ERP DB (`mas_gti`) has **0** procedures referencing `cc_file` or `web_security` — the plaintext cards do **not** propagate into Sage. (Sage's native CC module is in fact **disabled**`SY_Company.CreditCardEnable = N`, `AR_CustomerCreditCard` = 0 rows — so Sage stores no cards at all and is not a cardholder-data location.) Exposure is **contained to the GTIware databases** (15 office DBs) on `GTI-INV-SQL` (`192.168.8.62`) — the **same SQL instance the public website connects to**, which is exactly how the website's SQLi reaches the card data (see **C0**). Secondary exposure surfaces: **database backups** (every backup of those DBs contains plaintext PAN + CVV) and **stale on-disk code/data copies** (`Old_bin`, `Old_code`).
**Fix preserves the feature.** Have GTIware **call the processor's API directly / use its hosted vault** (no card data stored on-prem), or at minimum **tokenize** (store a processor token; let the processor hold the PAN) — `gt_auto_process` keeps auto-billing by token while removing every stored PAN/CVV (and the backup liability). **CVV must never be stored (PCI Req 3.2)**, and any at-rest cardholder data must be **encrypted**.
---
## High Findings
### H1 — Production payment server is also a developer workstation
Installed on the live server: **Visual Studio Community 2015 and 2022, .NET 8 SDKs, MSBuild/Build Tools, TFS office integration, IIS 10 Express, Notepad++, WinRAR 7.22, OpenSSL 3.5.0**. Full application **source code is on the box** (128 `.vb` + 125 `.aspx.vb`, not precompiled). This massively expands attack surface and blast radius on the host that processes cardholder data. **Fix:** move development off the production host; deploy precompiled; remove SDKs/IDEs/dev tools.
### H2 — Remote-access sprawl, including end-of-life software
Present: **RealVNC Enterprise E4.2.8** (≈2009 — critically outdated, known auth-bypass-class issues), **ScreenConnect client v6.0.11622 (2018, stale)** alongside a current ScreenConnect, **Splashtop**, **Datto RMM + Datto EDR**, **Syncro**, plus GuruRMM. 6+ remote-management agents = large unmonitored access surface. **Fix:** remove RealVNC and the stale ScreenConnect immediately; rationalize to a single sanctioned remote-access tool; inventory who controls each.
### H3 — Debug/error information disclosure
`Web.config`: **`<compilation debug="true" …>`** in production and **`<customErrors mode="Off"/>`** present; login/employee code echo `ex.Message` to the page or via `errmsg`. Leaks stack traces, SQL errors, internal paths. **Fix:** `debug="false"`, `customErrors="On"` with a generic page, stop surfacing exception text to users.
### H4 — Listener accepts TLS 1.0/1.1
SChannel: **TLS 1.0 Server `Enabled=1`** (and TLS 1.1 at OS default = enabled); TLS 1.2 enabled. The public HTTPS endpoint therefore still negotiates deprecated TLS — a PCI listener finding. **Fix:** disable TLS 1.0/1.1 (SChannel server) after confirming no legacy client dependency; keep TLS 1.2.
### H5 — Session/credential handling
- Custom **Session-variable auth** (no ASP.NET Forms auth); **no session-ID regeneration on login** → session-fixation risk.
- **No `requireSSL`** and **no `httpOnlyCookies`** configured → cookies not marked Secure (site was HTTP-reachable until the 2026-06-03 HTTP→HTTPS redirect was added).
- **No MFA, no account lockout / rate limiting**; username = customer **account number** (guessable) → brute-force exposure.
- **Detection blind spot (see Appendix A):** the employee login returns **HTTP 200 on both success and failure** (no redirect-on-success), and the app logs **no failed login attempts** anywhere. App-level auth never touches the Windows Security log. The net effect is that a slow credential-guessing attack against staff or customer accounts would be **effectively invisible** — there is no lockout to stop it and no log to detect it after the fact.
- **Fix:** Secure+HttpOnly cookies, regenerate session on login, add lockout/throttling, **add failed-login logging (timestamp, username, source IP)**, consider MFA for employee/admin access.
### H6 — Database access model
Web app connects with a **single shared SQL login (`tom`)** that has full read on card and password columns (no column-level control); **connection strings with credentials are in `Web.config`** on the web server (15+ per-office DBs). **Fix:** least-privilege per-function accounts, remove blanket card/password read, protect/secret-manage connection strings, enable TDE at rest.
---
## Medium / Component Hygiene
- **Outdated third-party libraries in `bin`:** `AjaxControlToolkit 3.0.30930` (2008 — present but **not referenced**, remove it), `Microsoft.IdentityModel.Tokens` / `System.IdentityModel.Tokens.Jwt 5.1.2` (2017), `CyberSource SDK 1.4.10` (legacy), assorted GrapeCity ActiveReports versions. Inventory and update/remove.
- **`machineKey validation="SHA1"`** — move to SHA-256 (AES/HMACSHA256) with managed keys.
- **Source code resident on production** — remove; deploy build artifacts only.
- **OpenSSL 3.5.0 / WinRAR 7.22 / Chrome** on a server — patch or remove; reduce footprint.
---
## What is Acceptable (balanced view)
- **OS patching is current-ish:** Windows Server 2019, build 17763.8755, patched through **May 2026** (supported to 2029) — the OS itself is *not* the weak point.
- **Most data access is parameterized** (948 parameterized calls) — the SQLi exposure is a bounded set of concatenated queries, not pervasive.
- **The Sage 100 ERP DB** (`mas_gti`) stores **no** cardholder data — its native CC module is **disabled** (`CreditCardEnable = N`, `AR_CustomerCreditCard` = 0 rows; tokenization columns exist in the schema but are unused). The plaintext exposure is **entirely GTIware's card-on-file feature** (the internal PSA, staff-operated, storing into databases the website shares) — **not** Sage, and **not** the website's payment pages (which store nothing). The website's role is the *access path* (C0), not the storer.
- TLS 1.2 to CyberSource now works (payment outage fixed 2026-06-03).
---
## Prioritized Remediation Roadmap
**Now (days):**
1. Purge stored CVV (`cc_file.cc_code`); stop writing it.
2. `debug="false"`, `customErrors="On"`; HTML-encode `gt_errorpage` output; stop echoing exceptions.
3. Remove RealVNC 4.2.8 and the stale ScreenConnect v6 client.
4. Disable TLS 1.0/1.1 on the listener.
**Short term (weeks):**
5. Convert passwords to salted hashes; replace the email-the-password flow with reset tokens; force a global reset.
6. Parameterize the concatenated SQL (payment pages first); delete `quo()`.
7. Secure+HttpOnly cookies; session regeneration; login throttling/lockout.
8. Move card-on-file to the CyberSource token vault; purge/encrypt historical PAN columns.
**Structural (project):**
9. Separate development from the production host; deploy precompiled; remove dev tooling and source from prod.
10. Least-privilege DB accounts, secret management for connection strings, TDE; re-scope the merchant PCI SAQ after remediation.
---
## Appendix A — Intrusion / Brute-Force Log Review (2026-06-04)
**Question asked:** Is there evidence in the logs of anyone trying to brute-force the website logins (or the server)?
**Method (read-only, via GuruRMM):** 7 days of IIS request logs (`C:\inetpub\logs\LogFiles\W3SVC4`, ~52,000 requests, May 29 Jun 4) plus the Windows Security event log (4625 failed logons / 4624 successful logons, 7-day window; log retains 65 days back to 2026-03-31). No data was modified.
**Bottom line: NO evidence of a brute-force attack — not against the website logins, and not against the Windows server.**
### Website logins (IIS)
Two login endpoints on the live site: `/customer_login.aspx` (customer portal) and `/emp/employee-login.aspx` (staff portal).
| Endpoint | Success | Fail | Reading |
|---|---|---|---|
| `/customer_login.aspx` | 2,547 (HTTP 302) | 78 (HTTP 200) | ~97% success — normal traffic across 740 distinct IPs, each logging in a few times/week |
| `/emp/employee-login.aspx` | 77 (302) | 381 (200) | **Artifact, not failures** — see below |
- **Important interpretation correction:** the employee login returns **HTTP 200 on BOTH success and failure** (it does not use the post-redirect-get/302 pattern the customer login uses). Status code alone therefore does **not** indicate a failed staff login. The apparent "381 failures" is a measurement artifact. Confirmed by pulling full request timelines: the top "suspect" IP `160.3.157.9` (24 hits, "0 successes" by the 302 metric) is a single legitimate employee on an iPhone (consistent iOS 18.7 Safari UA all week) who POSTs the login and then loads `/emp/check-hours2.aspx` + `/emp/index.aspx` with real content — i.e. a logged-in employee checking a timecard. Every "failure-heavy" employee IP fits the same benign shape (residential IP, single consistent device UA, reaches protected pages).
- **No brute-force / credential-stuffing signature** anywhere: no single IP firing rapid high-volume login POSTs, no username-cycling at machine speed, no scanner/bot user-agents hammering the login.
- **Minor observation (not an attack):** scattered bursts of **HTTP 500** on post-login pages (`invoices.aspx`, `quotes.aspx`, `place_orders.aspx`, `billing-statements.aspx`, `online-payment-pnc.aspx`) — several IPs firing 817 identical 500s within a single second (automated, not human clicking). Source IPs incl. `201.146.179.166`, `64.178.182.162`, `205.185.107.49`, `172.87.137.60`. These read more like a buggy page than a campaign (different pages, different residential IPs, normal browser UAs), **but** given C3 (the SQL-injectable `quo()` path), a 500 on a login/data page is exactly what a quote character in input produces against that code — worth an app-side look at what those pages throw. IIS does not log POST bodies, so the attempted inputs are not recoverable from these logs.
### Windows server (Security log)
- **13** failed logons (4625) in 7 days — trivial volume (an internet-exposed Windows host sees thousands/day).
- **All 13 were LogonType 3 (SMB/network), 100% from internal LAN IPs, zero from any public IP.** No RDP (type 10) failures at all.
- Targeted usernames: 12 blank (null/anonymous SMB) + 1 `tomabens` (internal — a stale cached credential or service, not an attack).
- **No successful remote logons from any external IP** (4624 type 3/8/10).
- **Inference:** the absence of *any* external 4625 strongly indicates RDP/SMB are **not internet-exposed** (a reachable RDP port produces a relentless type-10 failure stream within hours). The only meaningful internet-facing attack surface is the web application on 443 — which is exactly where the risk in this report lives.
### Takeaway for remediation
The review found no attacker, but it confirmed the **detection gap** noted in H5: 200-on-both responses + no lockout + no failed-login logging mean a slow guessing attack would be invisible here. **Add failed-login logging and account lockout** (already on the roadmap) — without them, "has anyone tried to break in?" cannot be answered with confidence going forward. Re-run this same log review periodically.
*Read-only review. No card numbers, passwords, or POST bodies were retrieved or are reproduced.*
---
**Status:** Assessment complete 2026-06-03; intrusion/brute-force log review added 2026-06-04 (Appendix A). **No changes were made to the application, database, or data** (read-only throughout). Findings to be reviewed with the client (Steve Eastman / Tom) as priority security and PCI remediation. This report contains no card numbers or passwords.