Passed
Push — master ( 09cfdd...94b89f )
by Stefan
07:21 queued 03:30
created

Telepath::magic()   D

Complexity

Conditions 18
Paths 200

Size

Total Lines 80
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 80
rs 4.6966
c 0
b 0
f 0
cc 18
eloc 36
nc 200
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * ******************************************************************************
5
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
6
 * and GN4-2 consortia
7
 *
8
 * License: see the web/copyright.php file in the file structure
9
 * ******************************************************************************
10
 */
11
12
namespace core\diag;
13
14
use \Exception;
15
16
require_once(dirname(dirname(__DIR__)) . "/config/_config.php");
17
18
/**
19
 * The overall coordination class that runs all kinds of tests to find out where
20
 * and what is wrong. Operates on the realm of a user. Can do more magic if it
21
 * also knows which federation the user is currently positioned in, or even 
22
 * which exact hotspot to analyse.
23
 */
24
class Telepath extends AbstractTest {
25
26
    private $realm;
27
    private $visitedFlr;
28
    private $visitedHotspot;
29
    private $catProfile;
30
    private $dbIdP;
31
32
    /**
33
     *
34
     * @var string|null
35
     */
36
    private $idPFederation;
37
    private $testsuite;
38
39
    /**
40
     * prime the Telepath with info it needs to know to successfully meditate over the problem
41
     * @param string $realm the realm of the user
42
     * @param string|null $visitedFlr which NRO is the user visiting
43
     * @param string|null $visitedHotspot external DB ID of the hotspot he visited
44
     */
45
    public function __construct(string $realm, $visitedFlr = NULL, $visitedHotspot = NULL) {
46
        // Telepath is the first one in a chain, no previous inputs allowed
47
        if (isset($_SESSION) && isset($_SESSION["SUSPECTS"])) {
48
            unset($_SESSION["SUSPECTS"]);
49
        }
50
        if (isset($_SESSION) && isset($_SESSION["EVIDENCE"])) {
51
            unset($_SESSION["EVIDENCE"]);
52
        }
53
        // now fill with default values
54
        parent::__construct();
55
        $this->realm = $realm;
56
        $this->visitedFlr = $visitedFlr;
57
        $this->visitedHotspot = $visitedHotspot;
58
        $links = \core\Federation::determineIdPIdByRealm($realm);
59
        $this->catProfile = $links["CAT"];
60
        $this->dbIdP = $links["EXTERNAL"];
61
        $this->idPFederation = $links["FEDERATION"];
62
        // this is NULL if the realm is not known in either DB
63
        // if so, let's try a regex to extract the ccTLD if any
64
        $matches = [];
65
        if ($this->idPFederation === NULL && preg_match("/\.(..)$/", $realm, $matches)) {
66
            $this->idPFederation = strtoupper($matches[1]);
67
        }
68
        $this->loggerInstance->debug(4, "XYZ: IdP-side NRO is " . $this->idPFederation . "\n");
69
    }
70
71
    /* The eduroam OT monitoring has the following return codes:
72
     * 
73
74
      Status codes
75
76
      0 - O.K.
77
      -1 - Accept O.K. Reject No
78
      -2 - Reject O.K. Accept No
79
      -3 - Accept No Reject No
80
      -9 - system error
81
      -10 - Accept O.K. Reject timeou
82
      -11 - Accept O.K. Reject no EAP
83
      -20 - Reject O.K. Accept timeou
84
      -21 - Reject O.K. Accept no EAP
85
      -31 - Accept No  Reject timeou
86
      -32 - Accept Timeout Reject no
87
      -33 - Accept Timeout Reject timeou
88
      -35 - Accept No Reject no EAP
89
      -36 - Reject No Accept no EAP
90
      -37 - Reject No EAP Accept no EAP
91
      -40 - UDP test error
92
93
     */
94
95
    /**
96
     * ask the monitoring API about the things it knows
97
     * @param string $type which type of test to execute
98
     * @param string $param1 test-specific parameter number 1, if any
99
     * @param string $param2 test-specific parameter number 2, if any
100
     * @return array
101
     */
102
    private function genericAPIStatus($type, $param1 = NULL, $param2 = NULL) {
103
        $endpoints = [
104
            'tlr_test' => "https://monitor.eduroam.org/mapi/index.php?type=tlr_test&tlr=$param1",
105
            'federation_via_tlr' => "https://monitor.eduroam.org/mapi/index.php?type=federation_via_tlr&federation=$param1",
106
            'flrs_test' => "https://monitor.eduroam.org/mapi/index.php?type=flrs_test&federation=$param1",
107
            'flr_by_federation' => "https://monitor.eduroam.org/mapi/index.php?type=flr_by_federation&federation=$param2&with=$param1",
108
        ];
109
        $ignore = [
110
            'tlr_test' => 'tlr',
111
            'federation_via_tlr' => 'fed',
112
            'flrs_test' => 'fed',
113
            'flr_by_federation' => 'fed',
114
        ];
115
        $this->loggerInstance->debug(4, "Doing Monitoring API check with $endpoints[$type]\n");
116
        $jsonResult = \core\common\OutsideComm::downloadFile($endpoints[$type]);
117
        $this->loggerInstance->debug(4, "Monitoring API Result: $jsonResult\n");
118
        $decoded = json_decode($jsonResult, TRUE);
119
        $retval = [];
120
        $retval["RAW"] = $decoded;
121
        $atLeastOneFunctional = FALSE;
122
        $allFunctional = TRUE;
123
        if (!isset($decoded[$type]) || isset($decoded['ERROR'])) {
124
            $retval["STATUS"] = AbstractTest::STATUS_MONITORINGFAIL;
125
            return $retval;
126
        }
127
        foreach ($decoded[$type] as $instance => $resultset) {
128
            if ($instance == $ignore[$type]) {
129
                // don't care
130
                continue;
131
            }
132
            // TLR test has statuscode on this level, otherwise need to recurse
133
            // one more level
134
            switch ($type) {
135
                case "tlr_test":
136
                    switch ($resultset['status_code']) {
137
                        case 0:
138
                            $atLeastOneFunctional = TRUE;
139
                            break;
140
                        case 9: // monitoring itself has an error, no effect on our verdict
141
                        case -1: // Reject test fails, but we diagnose supposed-working connection, so no effect on our verdict
142
                        case -10: // same as previous
143
                        case -11: // same as previous
144
                            break;
145
                        default:
146
                            $allFunctional = FALSE;
147
                    }
148
                    break;
149
                default:
150
                    foreach ($resultset as $particularInstance => $particularResultset) {
151
                        switch ($particularResultset['status_code']) {
152
                            case 0:
153
                                $atLeastOneFunctional = TRUE;
154
                                break;
155
                            case 9: // monitoring itself has an error, no effect on our verdict
156
                            case -1: // Reject test fails, but we diagnose supposed-working connection, so no effect on our verdict
157
                            case -10: // same as previous
158
                            case -11: // same as previous
159
                                break;
160
                            default:
161
                                $allFunctional = FALSE;
162
                        }
163
                    }
164
            }
165
        }
166
167
        if ($allFunctional) {
168
            $retval["STATUS"] = AbstractTest::STATUS_GOOD;
169
            return $retval;
170
        }
171
        if ($atLeastOneFunctional) {
172
            $retval["STATUS"] = AbstractTest::STATUS_PARTIAL;
173
            return $retval;
174
        }
175
        $retval["STATUS"] = AbstractTest::STATUS_DOWN;
176
        return $retval;
177
    }
178
179
    /**
180
     * Are the ETLR servers in order?
181
     * @return array
182
     */
183
    private function checkEtlrStatus() {
184
        // TODO: we always check the European TLRs even though the connection in question might go via others and/or this one
185
        // needs a table to determine what goes where :-(
186
        $ret = $this->genericAPIStatus("tlr_test", "TLR_EU");
187
        $this->additionalFindings[AbstractTest::INFRA_ETLR][] = $ret;
188
        switch ($ret["STATUS"]) {
189
            case AbstractTest::STATUS_GOOD:
190
                unset($this->possibleFailureReasons[AbstractTest::INFRA_ETLR]);
191
                break;
192
            case AbstractTest::STATUS_PARTIAL:
193
            case AbstractTest::STATUS_MONITORINGFAIL:
194
                // one of the ETLRs is down, or there is a failure in the monitoring system? 
195
                // This probably doesn't impact the user unless he's unlucky and has his session fall into failover.
196
                // keep ETLR as a possible problem with original probability
197
                break;
198
            case AbstractTest::STATUS_DOWN:
199
                // Oh! Well if it is not international roaming, that still doesn't have an effect /in this case/. 
200
                if ($this->idPFederation == $this->visitedFlr) {
201
                    unset($this->possibleFailureReasons[AbstractTest::INFRA_ETLR]);
202
                    break;
203
                }
204
                // But it is about int'l roaming, and we are spot on here.
205
                // Raise probability by much (even monitoring is sometimes wrong, or a few minutes behind reality)
206
                $this->possibleFailureReasons[AbstractTest::INFRA_ETLR] = 0.95;
207
        }
208
    }
209
210
    /**
211
     * Is the uplink between an NRO server and the ETLRs in order?
212
     * @param string $whichSide
213
     * @return array
214
     */
215
    private function checkFedEtlrUplink($whichSide) {
216
        // TODO: we always check the European TLRs even though the connection in question might go via others and/or this one
217
        // needs a table to determine what goes where :-(
218
        switch ($whichSide) {
219
            case AbstractTest::INFRA_NRO_IDP:
220
                $fed = $this->idPFederation;
221
                $linkVariant = AbstractTest::INFRA_LINK_ETLR_NRO_IDP;
222
                break;
223
            case AbstractTest::INFRA_NRO_SP:
224
                $fed = $this->visitedFlr;
225
                $linkVariant = AbstractTest::INFRA_LINK_ETLR_NRO_SP;
226
                break;
227
            default:
228
                throw new Exception("This function operates on the IdP- or SP-side FLR, nothing else!");
229
        }
230
231
        $ret = $this->genericAPIStatus("federation_via_tlr", $fed);
232
        $this->additionalFindings[AbstractTest::INFRA_NRO_IDP][] = $ret;
233
        switch ($ret["STATUS"]) {
234
            case AbstractTest::STATUS_GOOD:
235
                unset($this->possibleFailureReasons[$whichSide]);
236
                unset($this->possibleFailureReasons[$linkVariant]);
237
                break;
238
            case AbstractTest::STATUS_PARTIAL:
239
                // a subset of the FLRs is down? This probably doesn't impact the user unless he's unlucky and has his session fall into failover.
240
                // keep FLR as a possible problem with original probability
241
                break;
242
            case AbstractTest::STATUS_DOWN:
243
                // Raise probability by much (even monitoring is sometimes wrong, or a few minutes behind reality)
244
                // if earlier test found the server itself to be the problem, keep it, otherwise put the blame on the link
245
                if ($this->possibleFailureReasons[$whichSide] != 0.95) {
246
                    $this->possibleFailureReasons[$linkVariant] = 0.95;
247
                }
248
        }
249
    }
250
251
    /**
252
     * Is the NRO server itself in order?
253
     * @param string $whichSide
254
     * @return array
255
     */
256
    private function checkFlrServerStatus($whichSide) {
257
        switch ($whichSide) {
258
            case AbstractTest::INFRA_NRO_IDP:
259
                $fed = $this->idPFederation;
260
                break;
261
            case AbstractTest::INFRA_NRO_SP:
262
                $fed = $this->visitedFlr;
263
                break;
264
            default:
265
                throw new Exception("This function operates on the IdP- or SP-side FLR, nothing else!");
266
        }
267
268
        $ret = $this->genericAPIStatus("flrs_test", $fed);
269
        $this->additionalFindings[$whichSide][] = $ret;
270
        switch ($ret["STATUS"]) {
271
            case AbstractTest::STATUS_GOOD:
272
                unset($this->possibleFailureReasons[$whichSide]);
273
                break;
274
            case AbstractTest::STATUS_PARTIAL:
275
                // a subset of the FLRs is down? This probably doesn't impact the user unless he's unlucky and has his session fall into failover.
276
                // keep FLR as a possible problem with original probability
277
                break;
278
            case AbstractTest::STATUS_DOWN:
279
                // Raise probability by much (even monitoring is sometimes wrong, or a few minutes behind reality)
280
                $this->possibleFailureReasons[$whichSide] = 0.95;
281
        }
282
    }
283
284
    /**
285
     * Does authentication traffic flow between a given source and destination NRO?
286
     * @return array
287
     */
288
    private function checkNROFlow() {
289
        return $this->genericAPIStatus("flr_by_federation", $this->idPFederation, $this->visitedFlr);
290
    }
291
292
    /**
293
     * Runs the CAT-internal diagnostics tests. Determines the state of the 
294
     * realm (and indirectly that of the links and statuses of involved proxies
295
     * and returns a judgment whether external Monitoring API tests are warranted
296
     * or not
297
     * @return boolean TRUE if external tests have to be run
298
     */
299
    private function CATInternalTests() {
300
        // we are expecting to get a REJECT from all runs, because that means the packet got through to the IdP.
301
        // (the ETLR sometimes does a "Reject instead of Ignore" but that is filtered out and changed into a timeout
302
        // by the test suite automatically, so it does not disturb the measurement)
303
        // If that's true, we can exclude two sources of problems (both proxy levels). Hooray!
304
        $allAreConversationReject = TRUE;
305
        $atLeastOneConversationReject = FALSE;
306
307
        foreach (CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'] as $probeindex => $probe) {
308
            $reachCheck = $this->testsuite->udpReachability($probeindex);
309
            if ($reachCheck != RADIUSTests::RETVAL_CONVERSATION_REJECT) {
310
                $allAreConversationReject = FALSE;
311
            } else {
312
                $atLeastOneConversationReject = TRUE;
313
            }
314
315
            $this->additionalFindings[AbstractTest::INFRA_ETLR][] = ["DETAIL" => $this->testsuite->consolidateUdpResult($probeindex)];
316
            $this->additionalFindings[AbstractTest::INFRA_NRO_IDP][] = ["DETAIL" => $this->testsuite->consolidateUdpResult($probeindex)];
317
            $this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][] = ["DETAIL" => $this->testsuite->consolidateUdpResult($probeindex)];
318
        }
319
320
        if ($allAreConversationReject) {
321
            $this->additionalFindings[AbstractTest::INFRA_ETLR][] = ["CONNCHECK" => RADIUSTests::RETVAL_CONVERSATION_REJECT];
322
            $this->additionalFindings[AbstractTest::INFRA_NRO_IDP][] = ["CONNCHECK" => RADIUSTests::RETVAL_CONVERSATION_REJECT];
323
            $this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][] = ["CONNCHECK" => RADIUSTests::RETVAL_CONVERSATION_REJECT];
324
            $this->additionalFindings[AbstractTest::INFRA_LINK_ETLR_NRO_IDP][] = ["LINKCHECK" => RADIUSTests::L_OK];
325
            // we have actually reached an IdP, so all links are good, and the
326
            // realm is routable in eduroam. So even if it exists in neither DB
327
            // we can exclude the NONEXISTENTREALM case
328
            unset($this->possibleFailureReasons[AbstractTest::INFRA_ETLR]);
329
            unset($this->possibleFailureReasons[AbstractTest::INFRA_NRO_IDP]);
330
            unset($this->possibleFailureReasons[AbstractTest::INFRA_LINK_ETLR_NRO_IDP]);
331
            unset($this->possibleFailureReasons[AbstractTest::INFRA_NONEXISTENTREALM]);
332
        }
333
334
        if ($atLeastOneConversationReject) {
335
            // at least we can be sure it exists
336
            unset($this->possibleFailureReasons[AbstractTest::INFRA_NONEXISTENTREALM]);
337
            // It could still be an IdP RADIUS problem in that some cert oddities 
338
            // in combination with the device lead to a broken auth
339
            // if there is nothing beyond the "REMARK" level, then it's not an IdP problem
340
            // otherwise, add the corresponding warnings and errors to $additionalFindings
341
            switch ($this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][0]['DETAIL']['level']) {
342
                case RADIUSTests::L_OK:
343
                case RADIUSTests::L_REMARK:
344
                    // both are fine - the IdP is working and the user problem
345
                    // is not on the IdP RADIUS level
346
                    $this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][] = ["ODDITYLEVEL" => $this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][0]['DETAIL']['level']];
347
                    unset($this->possibleFailureReasons[AbstractTest::INFRA_IDP_RADIUS]);
348
                    break;
349
                case RADIUSTests::L_WARN:
350
                    $this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][] = ["ODDITYLEVEL" => RADIUSTests::L_WARN];
351
                    $this->possibleFailureReasons[AbstractTest::INFRA_IDP_RADIUS] = 0.3; // possibly we found the culprit - if RADIUS server is misconfigured AND user is on a device which reacts picky about exactly this oddity.
352
                    break;
353
                case RADIUSTests::L_ERROR:
354
                    $this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][] = ["ODDITYLEVEL" => RADIUSTests::L_ERROR];
355
                    $this->possibleFailureReasons[AbstractTest::INFRA_IDP_RADIUS] = 0.8; // errors are never good, so we can be reasonably sure we've hit the spot!
356
            }
357
        }
358
    }
359
360
    private function determineTestsuiteParameters() {
361
        if ($this->catProfile > 0) {
362
            $profileObject = \core\ProfileFactory::instantiate($this->catProfile);
363
            $readinessLevel = $profileObject->readinessLevel();
364
365
            switch ($readinessLevel) {
366
                case \core\AbstractProfile::READINESS_LEVEL_SHOWTIME: 
367
                    // fall-througuh intended: use the data even if non-public but complete
368
                case \core\AbstractProfile::READINESS_LEVEL_SUFFICIENTCONFIG:
369
                    $this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][] = ["Profile" => $profileObject->identifier];
370
                    $this->testsuite = new RADIUSTests($this->realm, $profileObject->getRealmCheckOuterUsername(), $profileObject->getEapMethodsinOrderOfPreference(1), $profileObject->getCollapsedAttributes()['eap:server_name'], $profileObject->getCollapsedAttributes()["eap:ca_file"]);
371
                    break;
372
                case \core\AbstractProfile::READINESS_LEVEL_NOTREADY:
373
                    $this->additionalFindings[AbstractTest::INFRA_IDP_RADIUS][] = ["Profile" => "UNCONCLUSIVE"];
374
                    $this->testsuite = new RADIUSTests($this->realm, "anonymous@" . $this->realm);
375
                    break;
376
                default:
377
            }
378
        } else {
379
            $this->testsuite = new RADIUSTests($this->realm, "anonymous@" . $this->realm);
380
        }
381
    }
382
383
    /**
384
     * Does the main meditation job
385
     * @return array the findings
386
     */
387
    public function magic() {
388
389
        // simple things first: do we know anything about the realm, either
390
        // because it's a CAT participant or because it's in the eduroam DB?
391
        // if so, we can exclude the INFRA_NONEXISTENTREALM cause
392
        $this->additionalFindings[AbstractTest::INFRA_NONEXISTENTREALM]['DATABASE_STATUS'] = ["ID1" => $this->catProfile, "ID2" => $this->dbIdP];
393
        if ($this->catProfile != \core\Federation::UNKNOWN_IDP || $this->dbIdP != \core\Federation::UNKNOWN_IDP) {
394
            unset($this->possibleFailureReasons[AbstractTest::INFRA_NONEXISTENTREALM]);
395
        }
396
        // do we operate on a non-ambiguous, fully configured CAT profile? Then
397
        // we run the more thorough check, otherwise the shallow one.
398
        $this->determineTestSuiteParameters();
399
        // let's do the least amount of testing needed:
400
        // - The CAT reachability test already covers ELTRs, IdP NRO level and the IdP itself.
401
        //   if the realm maps to a CAT IdP, we can run the more thorough tests; otherwise just
402
        //   the normal shallow ones
403
        // these are the normal "realm check" tests covering ETLR, LINK_NRO_IDP, NRO, IDP_RADIUS
404
        $this->CATInternalTests();
405
        // - if the test does NOT go through, we need to find out which of the three is guilty
406
        // - then, the international "via ETLR" check can be used to find out if the IdP alone
407
        //   is guilty. If that one fails, the direct monitoring of servers and ETLRs themselves
408
        //   closes the loop.
409
        // let's see if the ETLRs are up
410
        if (array_key_exists(AbstractTest::INFRA_ETLR, $this->possibleFailureReasons)) {
411
            $this->checkEtlrStatus();
412
        }
413
414
        // then let's check the IdP's FLR, if we know the IdP federation at all
415
        if ($this->idPFederation !== NULL) {
416
            if (array_key_exists(AbstractTest::INFRA_NRO_IDP, $this->possibleFailureReasons)) {
417
                // first the direct connectivity to the server
418
                $this->checkFlrServerStatus(AbstractTest::INFRA_NRO_IDP);
419
            }
420
            // now let's theck the link
421
            if (array_key_exists(AbstractTest::INFRA_LINK_ETLR_NRO_IDP, $this->possibleFailureReasons)) {
422
                $this->checkFedEtlrUplink(AbstractTest::INFRA_NRO_IDP);
423
            }
424
        }
425
        // now, if we know the country the user is currently in, let's see 
426
        // if the NRO SP-side is up
427
        if ($this->visitedFlr !== NULL) {
428
            $this->checkFlrServerStatus(AbstractTest::INFRA_NRO_SP);
429
            // and again its uplink to the ETLR
430
            $this->checkFedEtlrUplink(AbstractTest::INFRA_NRO_SP);
431
        }
432
        // the last thing we can do (but it's a bit redundant): check the country-to-country link
433
        // it's only needed if all three and their links are up, but we want to exclude funny routing blacklists 
434
        // which occur only in the *combination* of source and dest
435
        // if there is an issue at that point, blame the SP: once a request
436
        // would have reached the ETLRs, things would be all good (assuming
437
        // perfection on the ETLRs here!). So the SP has a wrong config.
438
        if ($this->idPFederation !== NULL &&
439
                $this->visitedFlr !== NULL &&
440
                !array_key_exists(AbstractTest::INFRA_ETLR, $this->possibleFailureReasons) &&
441
                !array_key_exists(AbstractTest::INFRA_LINK_ETLR_NRO_IDP, $this->possibleFailureReasons) &&
442
                !array_key_exists(AbstractTest::INFRA_NRO_IDP, $this->possibleFailureReasons) &&
443
                !array_key_exists(AbstractTest::INFRA_LINK_ETLR_NRO_SP, $this->possibleFailureReasons) &&
444
                !array_key_exists(AbstractTest::INFRA_NRO_SP, $this->possibleFailureReasons)
445
        ) {
446
            $countryToCountryStatus = $this->checkNROFlow();
447
            $this->additionalFindings[AbstractTest::INFRA_NRO_SP][] = $countryToCountryStatus;
448
            $this->additionalFindings[AbstractTest::INFRA_ETLR][] = $countryToCountryStatus;
449
            $this->additionalFindings[AbstractTest::INFRA_NRO_IDP][] = $countryToCountryStatus;
450
            switch ($countryToCountryStatus["STATUS"]) {
451
                case AbstractTest::STATUS_GOOD:
452
                    // all routes work
453
                    break;
454
                case AbstractTest::STATUS_PARTIAL:
455
                // at least one, or even all have a routing problem
456
                case AbstractTest::STATUS_DOWN:
457
                    // that's rather telling.
458
                    $this->possibleFailureReasons[AbstractTest::INFRA_NRO_SP] = 0.95;
459
            }
460
        }
461
462
        $this->normaliseResultSet();
463
464
        $_SESSION["SUSPECTS"] = $this->possibleFailureReasons;
465
        $_SESSION["EVIDENCE"] = $this->additionalFindings;
466
        return ["SUSPECTS" => $this->possibleFailureReasons, "EVIDENCE" => $this->additionalFindings];
467
    }
468
469
}
470