|
| 1 | +""" |
| 2 | +Example: Cybersecurity Knowledge Graph with Inconsistency Detection |
| 3 | +==================================================================== |
| 4 | +This example builds a small cybersecurity knowledge graph containing |
| 5 | +network assets (servers and workstations), the software they run, and |
| 6 | +real CVEs (Common Vulnerabilities and Exposures) from the National |
| 7 | +Vulnerability Database (NVD) that affect that software. |
| 8 | +
|
| 9 | +CVSS scores are normalised to [0, 1] by dividing by 10 to serve as |
| 10 | +PyReason annotation bounds. |
| 11 | +
|
| 12 | +Graph structure: |
| 13 | + [asset] --runs--> [software] --has_cve--> [CVE] |
| 14 | +
|
| 15 | +Two types of inconsistency are demonstrated: |
| 16 | +
|
| 17 | + 1. Same-predicate non-overlapping bounds (monotonic reasoning violation): |
| 18 | + Two data sources assert conflicting severity scores for the same CVE. |
| 19 | + The bounds do not overlap so PyReason resolves to [0.0, 1.0]. |
| 20 | +
|
| 21 | + 2. Inconsistent Predicate List (IPL) conflict: |
| 22 | + "vulnerable" and "patched" are declared as mutually exclusive. |
| 23 | + Asserting both for the same asset creates a contradiction which |
| 24 | + PyReason resolves to [0.0, 1.0] on both predicates. |
| 25 | +
|
| 26 | +Real CVEs used: |
| 27 | + - CVE-2021-3156 sudo 1.9.5p1 CVSS 7.8 CWE-121 (heap buffer overflow) |
| 28 | + - CVE-2022-0185 Linux Kernel 5.1 CVSS 8.4 CWE-121 (stack overflow) |
| 29 | + - CVE-2022-26923 OpenSSL 3.0.1 CVSS 7.5 CWE-415 (double free) |
| 30 | +""" |
| 31 | + |
| 32 | +import pyreason as pr |
| 33 | +import networkx as nx |
| 34 | + |
| 35 | +# Reset PyReason to a clean state |
| 36 | +pr.reset() |
| 37 | +pr.reset_rules() |
| 38 | + |
| 39 | +# ================================ CREATE GRAPH ================================ |
| 40 | +g = nx.DiGraph() |
| 41 | + |
| 42 | +# Asset nodes -- servers and workstations in a small enterprise network |
| 43 | +g.add_nodes_from(['web_server', 'workstation_1', 'dev_server']) |
| 44 | + |
| 45 | +# Software nodes -- specific vulnerable versions |
| 46 | +g.add_nodes_from(['sudo_1_9_5p1', 'linux_kernel_5_1', 'openssl_3_0_1']) |
| 47 | + |
| 48 | +# CVE nodes -- real vulnerability identifiers from NVD |
| 49 | +g.add_nodes_from(['CVE_2021_3156', 'CVE_2022_0185', 'CVE_2022_26923']) |
| 50 | + |
| 51 | +# Asset --> Software edges (which asset runs which software version) |
| 52 | +g.add_edge('web_server', 'sudo_1_9_5p1', runs=1) |
| 53 | +g.add_edge('workstation_1', 'linux_kernel_5_1', runs=1) |
| 54 | +g.add_edge('dev_server', 'openssl_3_0_1', runs=1) |
| 55 | + |
| 56 | +# Software --> CVE edges (which CVE affects which software version) |
| 57 | +g.add_edge('sudo_1_9_5p1', 'CVE_2021_3156', has_cve=1) |
| 58 | +g.add_edge('linux_kernel_5_1', 'CVE_2022_0185', has_cve=1) |
| 59 | +g.add_edge('openssl_3_0_1', 'CVE_2022_26923', has_cve=1) |
| 60 | + |
| 61 | +# ================================ CONFIGURE =================================== |
| 62 | +pr.settings.verbose = True |
| 63 | +pr.settings.atom_trace = True # Enable atom trace for full explainability |
| 64 | +pr.settings.inconsistency_check = True # Enable inconsistency detection (default) |
| 65 | + |
| 66 | +# ================================ LOAD GRAPH ================================== |
| 67 | +pr.load_graph(g) |
| 68 | + |
| 69 | +# Declare vulnerable and patched as inconsistent predicates |
| 70 | +# When vulnerable(x):[l,u] is set, PyReason automatically sets |
| 71 | +# patched(x):[1-u, 1-l] -- and vice versa |
| 72 | +pr.add_inconsistent_predicate('vulnerable', 'patched') |
| 73 | + |
| 74 | +# ================================ ADD RULES =================================== |
| 75 | +# Rule 1: If an asset runs software that has a CVE, the asset is at risk |
| 76 | +# This is the core two-hop transitive inference: asset --> software --> CVE |
| 77 | +pr.add_rule(pr.Rule( |
| 78 | + 'at_risk(x) <- runs(x,y), has_cve(y,z)', |
| 79 | + 'exposure_rule' |
| 80 | +)) |
| 81 | + |
| 82 | +# Rule 2: An asset that is at risk is also vulnerable with high confidence |
| 83 | +# This chains off exposure_rule and also triggers the IPL for patched |
| 84 | +pr.add_rule(pr.Rule( |
| 85 | + 'vulnerable(x):[0.8,1.0] <- at_risk(x)', |
| 86 | + 'vulnerability_rule' |
| 87 | +)) |
| 88 | + |
| 89 | +# ================================ ADD FACTS =================================== |
| 90 | +# CVE severity scores from NVD, normalised to [0,1] by dividing by 10 |
| 91 | +# CVE-2021-3156: CVSS 7.8 / 10 = 0.78 |
| 92 | +pr.add_fact(pr.Fact('severity(CVE_2021_3156):[0.78,0.78]', 'sudo_cve_severity', 0, 2)) |
| 93 | +# CVE-2022-0185: CVSS 8.4 / 10 = 0.84 |
| 94 | +pr.add_fact(pr.Fact('severity(CVE_2022_0185):[0.84,0.84]', 'kernel_cve_severity', 0, 2)) |
| 95 | +# CVE-2022-26923: CVSS 7.5 / 10 = 0.75 |
| 96 | +pr.add_fact(pr.Fact('severity(CVE_2022_26923):[0.75,0.75]', 'openssl_cve_severity', 0, 2)) |
| 97 | + |
| 98 | +# ---- Inconsistency Demo 1: Monotonic reasoning violation ---- |
| 99 | +# Two data sources disagree on the severity of CVE_2021_3156 |
| 100 | +# [0.8, 1.0] and [0.0, 0.1] do not overlap -- PyReason flags the conflict |
| 101 | +# and resolves severity(CVE_2021_3156) to [0.0, 1.0] (complete uncertainty) |
| 102 | +pr.add_fact(pr.Fact('severity(CVE_2021_3156):[0.8,1.0]', 'severity_source_A', 0, 2)) |
| 103 | +pr.add_fact(pr.Fact('severity(CVE_2021_3156):[0.0,0.1]', 'severity_source_B', 0, 2)) |
| 104 | + |
| 105 | +# ---- Inconsistency Demo 2: Inconsistent Predicate List (IPL) conflict ---- |
| 106 | +# Asset management DB says web_server was patched -- high confidence |
| 107 | +pr.add_fact(pr.Fact('patched(web_server):[0.9,1.0]', 'patch_db_fact', 0, 2)) |
| 108 | +# Vulnerability scanner says web_server is vulnerable -- also high confidence |
| 109 | +# Since vulnerable and patched are in the IPL, this creates a contradiction |
| 110 | +# PyReason resolves both to [0.0, 1.0] and logs the conflict in the trace |
| 111 | +pr.add_fact(pr.Fact('vulnerable(web_server):[0.9,1.0]', 'vuln_scanner_fact', 0, 2)) |
| 112 | + |
| 113 | +# ================================ REASON ====================================== |
| 114 | +print('=' * 60) |
| 115 | +print('Running PyReason -- Cybersecurity Knowledge Graph') |
| 116 | +print('=' * 60) |
| 117 | +interpretation = pr.reason(timesteps=2) |
| 118 | + |
| 119 | +# ================================ VIEW RESULTS ================================ |
| 120 | +print('\n' + '=' * 60) |
| 121 | +print('Assets at risk (inferred by exposure_rule)') |
| 122 | +print('=' * 60) |
| 123 | +dataframes = pr.filter_and_sort_nodes(interpretation, ['at_risk']) |
| 124 | +for t, df in enumerate(dataframes): |
| 125 | + print(f'\nTIMESTEP {t}:') |
| 126 | + print(df) |
| 127 | + |
| 128 | +print('\n' + '=' * 60) |
| 129 | +print('CVE Severity (Demo 1: monotonic violation on CVE_2021_3156)') |
| 130 | +print('=' * 60) |
| 131 | +dataframes = pr.filter_and_sort_nodes(interpretation, ['severity']) |
| 132 | +for t, df in enumerate(dataframes): |
| 133 | + print(f'\nTIMESTEP {t}:') |
| 134 | + print(df) |
| 135 | + |
| 136 | +print('\n' + '=' * 60) |
| 137 | +print('Vulnerable / Patched (Demo 2: IPL conflict on web_server)') |
| 138 | +print('=' * 60) |
| 139 | +dataframes = pr.filter_and_sort_nodes(interpretation, ['vulnerable', 'patched']) |
| 140 | +for t, df in enumerate(dataframes): |
| 141 | + print(f'\nTIMESTEP {t}:') |
| 142 | + print(df) |
| 143 | + |
| 144 | +# ================================ VIEW TRACE ================================== |
| 145 | +print('\n' + '=' * 60) |
| 146 | +print('Rule Trace (full explainability)') |
| 147 | +print('=' * 60) |
| 148 | +node_trace, edge_trace = pr.get_rule_trace(interpretation) |
| 149 | +print('\nNode trace:') |
| 150 | +print(node_trace.to_string()) |
| 151 | + |
| 152 | +if not edge_trace.empty: |
| 153 | + print('\nEdge trace:') |
| 154 | + print(edge_trace.to_string()) |
| 155 | + |
| 156 | +# Save the rule trace to CSV files for further inspection |
| 157 | +pr.save_rule_trace(interpretation) |
| 158 | +print('\nRule trace saved to current directory.') |
0 commit comments