Catalyst / admin/Synapse-NetscanXi 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-NetscanXi

public

Network Scanning, Vulnerability and Compliance Application

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Synapse-NetscanXi / NetscanXiVersion13 / app / compliance.py 16144 B · main
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
"""
Compliance & credential-exposure analysis (v6).

Two responsibilities, both DEFENSIVE:

1. weak_auth_findings(host)
   Detect *credential-exposure* and weak-authentication risk on a host from
   what the scan already saw: plaintext login services, exposed admin/database
   ports, unauthenticated APIs, etc. This reports RISK. It never captures,
   transmits, brute-forces, or stores anyone's credentials.

2. assess_host(host)
   Cross-reference each host's findings against the supported regulatory /
   audit frameworks - PCI DSS, GDPR, ISO 27001, SOC 2, HIPAA, Cyber
   Essentials, and (v8-r3) NIST CSF and NCSC CAF - returning per-framework
   pass/fail control mappings so the dashboard can show where a host
   would put an organisation out of compliance.

Both are pure functions over the parsed host dict (see parser.py), so they are
trivially unit-testable and add no new scan capability on their own.
"""

# ---------------------------------------------------------------------------
# Service knowledge base
# ---------------------------------------------------------------------------
# Services that authenticate users but do so over CLEARTEXT (no transport
# encryption) -> credentials are exposed on the wire.
CLEARTEXT_AUTH_SERVICES = {
    21:  ("FTP", "ftp"),
    23:  ("Telnet", "telnet"),
    25:  ("SMTP (no STARTTLS)", "smtp"),
    80:  ("HTTP", "http"),
    110: ("POP3", "pop3"),
    143: ("IMAP", "imap"),
    389: ("LDAP (cleartext)", "ldap"),
    512: ("rexec", "exec"),
    513: ("rlogin", "login"),
    514: ("rsh", "shell"),
    1433:("MSSQL", "ms-sql-s"),
    3306:("MySQL", "mysql"),
    5432:("PostgreSQL", "postgresql"),
    5900:("VNC", "vnc"),
    6379:("Redis", "redis"),
    11211:("Memcached", "memcached"),
    27017:("MongoDB", "mongodb"),
}

# Service ports that expose remote administration / data stores. Exposure to
# the network is itself a finding even when encrypted.
ADMIN_DATASTORE_PORTS = {
    22:   "SSH (remote admin)",
    1433: "MSSQL database",
    3306: "MySQL database",
    3389: "RDP (remote desktop)",
    5432: "PostgreSQL database",
    5900: "VNC remote desktop",
    6379: "Redis (often no auth)",
    9200: "Elasticsearch (often no auth)",
    11211:"Memcached (no auth)",
    27017:"MongoDB (often no auth)",
    2375: "Docker API (unauthenticated)",
}

# Datastores/APIs that are unauthenticated by default - exposure == open access.
DEFAULT_NO_AUTH_PORTS = {
    2375: "Docker Engine API",
    6379: "Redis",
    9200: "Elasticsearch",
    11211:"Memcached",
    27017:"MongoDB",
}

SEV_ORDER = {"critical": 3, "high": 2, "warn": 1, "info": 0}


def _ports(host):
    """Yield (portnum:int, detail dict) for each open port on the host."""
    for pd in host.get("port_details", []):
        try:
            yield int(pd.get("port")), pd
        except (TypeError, ValueError):
            continue


def _has_tls(detail):
    """Best-effort: did the scan see TLS/SSL on this port?"""
    svc = (detail.get("service") or "").lower()
    extra = (detail.get("extrainfo") or "").lower()
    if "ssl" in svc or "tls" in svc or "https" in svc:
        return True
    if "ssl" in extra or "tls" in extra:
        return True
    for s in detail.get("scripts", []):
        if "ssl" in (s.get("id", "").lower()):
            return True
    return False


# ---------------------------------------------------------------------------
# 1) Credential-exposure / weak-auth detection (defensive)
# ---------------------------------------------------------------------------
def weak_auth_findings(host):
    """Return a list of credential-exposure findings for a host.

    Each finding: {id, title, severity, port, detail, remediation}
    """
    findings = []

    for portnum, detail in _ports(host):
        svc = (detail.get("service") or "").lower()

        # Cleartext authentication service -> credentials exposed on the wire.
        if portnum in CLEARTEXT_AUTH_SERVICES and not _has_tls(detail):
            name, _ = CLEARTEXT_AUTH_SERVICES[portnum]
            # HTTP is only a credential issue if something logs in over it; we
            # flag it as a warning rather than critical.
            sev = "warn" if portnum == 80 else "high"
            findings.append({
                "id": f"cleartext-{portnum}",
                "title": f"Cleartext authentication: {name}",
                "severity": sev,
                "port": f"{portnum}/{detail.get('protocol','tcp')}",
                "detail": f"{name} accepts logins without transport encryption; "
                          "credentials can be captured by anyone on-path.",
                "remediation": f"Disable {name} or require TLS (e.g. FTPS/SMTPS/"
                               "LDAPS/HTTPS) and rotate any exposed credentials.",
            })

        # Default-no-auth datastore/API exposed to the network.
        if portnum in DEFAULT_NO_AUTH_PORTS:
            name = DEFAULT_NO_AUTH_PORTS[portnum]
            findings.append({
                "id": f"noauth-{portnum}",
                "title": f"Unauthenticated service exposed: {name}",
                "severity": "critical",
                "port": f"{portnum}/{detail.get('protocol','tcp')}",
                "detail": f"{name} is reachable on the network and is commonly "
                          "deployed without authentication, allowing data access "
                          "or takeover.",
                "remediation": f"Bind {name} to localhost, enable authentication, "
                               "and firewall the port.",
            })

        # Remote admin / database surface exposed.
        elif portnum in ADMIN_DATASTORE_PORTS:
            name = ADMIN_DATASTORE_PORTS[portnum]
            findings.append({
                "id": f"admin-exposed-{portnum}",
                "title": f"Remote admin/data surface exposed: {name}",
                "severity": "warn",
                "port": f"{portnum}/{detail.get('protocol','tcp')}",
                "detail": f"{name} is reachable on the network; ensure strong "
                          "authentication, MFA where possible, and IP allow-lists.",
                "remediation": "Restrict by firewall/VPN, enforce strong "
                               "credentials + MFA, and monitor access.",
            })

    # De-dupe by id (a port could match more than one rule path).
    uniq, seen = [], set()
    for f in sorted(findings, key=lambda x: -SEV_ORDER.get(x["severity"], 0)):
        if f["id"] in seen:
            continue
        seen.add(f["id"])
        uniq.append(f)
    return uniq


# ---------------------------------------------------------------------------
# 2) Regulatory compliance cross-reference
# ---------------------------------------------------------------------------
# Map a generic "issue kind" to the specific control it violates in each
# framework. Keeps the mapping auditable in one place.
FRAMEWORKS = ("PCI DSS", "GDPR", "ISO 27001", "SOC 2", "HIPAA",
              "Cyber Essentials", "NIST CSF", "NCSC CAF")

# issue kind -> {framework: control-reference}
CONTROL_MAP = {
    "cleartext_auth": {
        "PCI DSS":   "4.2.1 / 8.3.1 - strong crypto for auth over open networks",
        "GDPR":      "Art. 32 - encryption of personal data in transit",
        "ISO 27001": "A.8.24 / A.5.14 - cryptography & secure transfer",
        "SOC 2":     "CC6.7 - encryption of data in transit",
        "HIPAA":     "164.312(e)(1) - transmission security",
        "Cyber Essentials": "Secure Configuration - disable insecure/cleartext services",
        "NIST CSF":  "PR.DS-02 - data-in-transit is protected (encryption)",
        "NCSC CAF":  "B3.c Stored/transmitted data - protect data in transit with encryption",
    },
    "unauth_service": {
        "PCI DSS":   "7.2 / 8.2 - restrict access & identify users",
        "GDPR":      "Art. 32 - access control to personal data",
        "ISO 27001": "A.5.15 / A.8.3 - access control",
        "SOC 2":     "CC6.1 - logical access controls",
        "HIPAA":     "164.312(a)(1) - access control",
        "Cyber Essentials": "User Access Control - no unauthenticated access to services",
        "NIST CSF":  "PR.AA-01/05 - identities authenticated; access enforced",
        "NCSC CAF":  "B2.a Identity verification & authentication - no unauthenticated access",
    },
    "admin_exposed": {
        "PCI DSS":   "1.3 / 8.3 - restrict inbound admin access",
        "GDPR":      "Art. 32 - security of processing",
        "ISO 27001": "A.8.20 / A.8.22 - network security & segregation",
        "SOC 2":     "CC6.6 - perimeter / boundary protection",
        "HIPAA":     "164.312(a)(1) - access control",
        "Cyber Essentials": "Firewalls - restrict inbound admin/remote-access services",
        "NIST CSF":  "PR.IR-01 - networks protected; PR.AA-05 least-privilege access",
        "NCSC CAF":  "B4.a/B2.c Secure config & privileged access - restrict remote admin",
    },
    "unencrypted_web": {
        "PCI DSS":   "4.2.1 - encrypt transmission of cardholder data",
        "GDPR":      "Art. 32 - encryption in transit",
        "ISO 27001": "A.8.24 - use of cryptography",
        "SOC 2":     "CC6.7 - encryption in transit",
        "HIPAA":     "164.312(e)(1) - transmission security",
        "Cyber Essentials": "Secure Configuration - enforce HTTPS on web services",
        "NIST CSF":  "PR.DS-02 - data-in-transit protected (enforce TLS)",
        "NCSC CAF":  "B3.c Data in transit - enforce encrypted (HTTPS) transport",
    },
    "known_vuln": {
        "PCI DSS":   "6.3.1 / 11.3 - manage vulnerabilities & patch",
        "GDPR":      "Art. 32(1)(d) - regular testing of security",
        "ISO 27001": "A.8.8 - management of technical vulnerabilities",
        "SOC 2":     "CC7.1 - vulnerability detection & remediation",
        "HIPAA":     "164.308(a)(1)(ii)(A) - risk analysis / management",
        "Cyber Essentials": "Security Update Management - patch known vulnerabilities",
        "NIST CSF":  "ID.RA-01 / PR.PS-02 - identify vulnerabilities; manage patches",
        "NCSC CAF":  "B4.d Vulnerability management - identify & remediate known flaws",
    },
    "kev_vuln": {
        "PCI DSS":   "6.3.1 - critical/known-exploited vulnerabilities",
        "GDPR":      "Art. 32 - state-of-the-art security measures",
        "ISO 27001": "A.8.8 - technical vulnerability management",
        "SOC 2":     "CC7.1 - timely remediation of known issues",
        "HIPAA":     "164.308(a)(1) - security management process",
        "Cyber Essentials": "Security Update Management - apply critical patches within 14 days",
        "NIST CSF":  "ID.RA-06 / RS.MA - prioritise & remediate known-exploited vulns",
        "NCSC CAF":  "B4.d Vulnerability management - urgent remediation of exploited flaws",
    },
    "expiring_cert": {
        "PCI DSS":   "4.2.1 - maintain valid strong cryptography",
        "GDPR":      "Art. 32 - ensure ongoing confidentiality/integrity",
        "ISO 27001": "A.8.24 - key & certificate lifecycle management",
        "SOC 2":     "CC6.7 - maintain encryption controls",
        "HIPAA":     "164.312(e)(1) - transmission security",
        "Cyber Essentials": "Secure Configuration - maintain valid TLS certificates",
        "NIST CSF":  "PR.DS-02 / PR.PS-01 - manage certificate & crypto lifecycle",
        "NCSC CAF":  "B3.c Data in transit - maintain valid certificates & strong crypto",
    },
    "ad_weakness": {
        "PCI DSS":   "8.3 / 2.2 - secure authentication & hardened configuration",
        "GDPR":      "Art. 32 - security of processing",
        "ISO 27001": "A.5.15 / A.8.5 - access control & secure authentication",
        "SOC 2":     "CC6.1 - logical access controls",
        "HIPAA":     "164.312(a)(1) - access control",
        "Cyber Essentials": "User Access Control - harden directory authentication",
        "NIST CSF":  "PR.AA-01/03 - manage identities & harden authentication",
        "NCSC CAF":  "B2.a/B2.c Identity & access control - harden directory authentication",
    },
}


def _issue_kinds(host, weak):
    """Derive the set of generic issue kinds present on a host."""
    kinds = {}  # kind -> list of human evidence strings

    def add(kind, evidence):
        kinds.setdefault(kind, [])
        if evidence not in kinds[kind]:
            kinds[kind].append(evidence)

    for f in weak:
        if f["id"].startswith("cleartext-"):
            add("cleartext_auth", f"{f['title']} ({f['port']})")
        elif f["id"].startswith("noauth-"):
            add("unauth_service", f"{f['title']} ({f['port']})")
        elif f["id"].startswith("admin-exposed-"):
            add("admin_exposed", f"{f['title']} ({f['port']})")

    # Unencrypted web specifically (port 80 without TLS).
    for portnum, detail in _ports(host):
        if portnum == 80 and not _has_tls(detail):
            add("unencrypted_web", "HTTP without TLS (80/tcp)")

    # Vulnerabilities -> patch-management controls.
    for v in host.get("vulns", []):
        if v.get("kev"):
            add("kev_vuln", f"Known-exploited: {v.get('id','')}")
        else:
            add("known_vuln", f"{v.get('id','')}")

    # v7: expiring/expired/weak TLS certificates (from cert tracking).
    for c in host.get("certificates", []):
        if c.get("status") in ("expired", "expiring", "weak"):
            label = {"expired": "Expired", "expiring": "Expiring soon",
                     "weak": "Weak"}.get(c["status"], c["status"])
            add("expiring_cert",
                f"{label} cert on {c.get('port','?')} "
                f"({c.get('subject') or c.get('issuer') or 'unknown'})")

    # v7: Active Directory / Domain Controller hardening findings.
    for f in host.get("ad_findings", []):
        if f.get("severity") in ("warn", "high", "critical"):
            add("ad_weakness", f.get("title", "AD/DC weakness"))

    return kinds


def assess_host(host, weak=None):
    """Cross-reference a host against all frameworks.

    Returns:
      {
        "status": "compliant" | "at_risk",
        "frameworks": {
            "PCI DSS": {"status": "fail"|"pass", "controls": [ {control, kind, evidence}... ]},
            ... one per framework ...
        },
        "issue_count": int,
      }
    """
    if weak is None:
        weak = weak_auth_findings(host)
    kinds = _issue_kinds(host, weak)

    frameworks = {}
    for fw in FRAMEWORKS:
        controls = []
        for kind, evidence in kinds.items():
            ref = CONTROL_MAP.get(kind, {}).get(fw)
            if ref:
                controls.append({
                    "kind": kind,
                    "control": ref,
                    "evidence": evidence,
                })
        frameworks[fw] = {
            "status": "fail" if controls else "pass",
            "controls": controls,
        }

    any_fail = any(f["status"] == "fail" for f in frameworks.values())
    return {
        "status": "at_risk" if any_fail else "compliant",
        "frameworks": frameworks,
        "issue_count": sum(len(v) for v in kinds.values()),
    }


def summarize(hosts):
    """Site-wide compliance roll-up across all hosts for the dashboard."""
    per_fw = {fw: {"pass": 0, "fail": 0, "failing_hosts": []} for fw in FRAMEWORKS}
    at_risk_hosts = 0
    # v9: passive-only hosts carry no compliance assessment (no active probe
    # data), so they are excluded from the framework roll-up.
    assessed = 0
    for h in hosts:
        comp = h.get("compliance") or assess_host(h)
        if not comp or "frameworks" not in comp:
            continue
        assessed += 1
        if comp["status"] == "at_risk":
            at_risk_hosts += 1
        for fw, data in comp["frameworks"].items():
            if data["status"] == "fail":
                per_fw[fw]["fail"] += 1
                per_fw[fw]["failing_hosts"].append(h.get("ip", ""))
            else:
                per_fw[fw]["pass"] += 1
    return {
        "frameworks": per_fw,
        "hosts_total": assessed,
        "hosts_at_risk": at_risk_hosts,
        "hosts_compliant": assessed - at_risk_hosts,
    }