Skip to content
Snippets Groups Projects
GraphDataRepository.php 20.17 KiB
<?php

namespace App\Repository;

use App\Models\Danger;
use App\Models\DangerLevelEvaluation;
use App\Models\Evaluation;
use App\Models\EvaluationMaturityAnswer;
use App\Models\Measure;
use App\Models\MeasureLevel;
use App\Models\Organization;
use App\Models\Scenario;
use App\Models\Territory;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class GraphDataRepository
{
    private EvaluationRepository $repository;

    public function __construct(EvaluationRepository $repository)
    {
        $this->repository = $repository;
    }

    public function getMeasuresDataForEvaluation(Evaluation $evaluation): array
    {
        $measureLevels = collect($evaluation->measureLevels)->sortBy(function (MeasureLevel $ml) {
            return $ml->measure->fundamental;
        });

        $levels = $measureLevels->map(function (MeasureLevel $level) {
            return $level->actual_level;
        });

        $labels = $measureLevels->map(function (MeasureLevel $level) {
            return $level->measure->short_name;
        });

        return [
            'labels' => $labels->values(),
            'data' => $levels->values(),
        ];
    }

    /**
     * For each danger, return the exposition value taking into account the danger level that was selected.
     */
    public function getExpositionDataForEvaluation(Evaluation $evaluation): array
    {
        $measureLevels = collect($evaluation->measureLevels);
        $dangerLevels = collect($evaluation->dangerLevels);

        // For each danger, add all points of all measures that apply to the danger scenarios

        $dangers = Danger::all();

        $labels = [];
        $values = [];

        foreach ($dangers as $danger) {
            /*
             * @var Danger $danger
             */
            $labels[] = $danger->name;

            $v = $danger->scenarios->reduce(function ($acc, Scenario $scenario) use ($measureLevels) {
                $exposition = $this->getExpositionPointsForScenario($scenario, $measureLevels);

                return $acc + $exposition;
            }, 0);

            // Get selected danger level
            $dangerLevel = $dangerLevels->first(function (DangerLevelEvaluation $dl) use ($danger) {
                return $dl->danger_id === $danger->id;
            });

            // the points for this danger are the total points for this danger,
            // divided by the number of appllicable scenarios
            $values[] = $v / $danger->scenarios->count() * $dangerLevel->level->getPointsAttribute();
        }

        $total = collect($values)->sum();

        $percentages = collect($values)->map(function ($val) use ($total) {
            if (0.0 === $val || 0.0 === $total) {
                return 10;
            }

            return max(10, $val / $total * 100);
        });

        return [
            'labels' => $labels,
            'data' => $percentages,
        ];
    }

    public function getRisksDataForEvaluation(Evaluation $evaluation): array
    {
        $measureLevels = collect($evaluation->measureLevels);

        // For each danger, add all points of all measures that apply to the danger scenarios

        $dangers = Danger::all();

        $labels = [];
        $values = [];

        foreach ($dangers as $danger) {
            /*
             * @var Danger $danger
             */
            $labels[] = $danger->name;

            $v = $danger->scenarios->reduce(function ($acc, Scenario $scenario) use ($measureLevels) {
                $exposition = $this->getExpositionPointsForScenario($scenario, $measureLevels);

                return $acc + $exposition;
            }, 0);

            $values[] = max(10, $v / $danger->scenarios->count());
        }

        return [
            'labels' => $labels,
            'data' => $values,
        ];
    }

    public function getFutureRisksDataForEvaluation(Evaluation $evaluation): array
    {
        $measureLevels = collect($evaluation->measureLevels);

        // For each danger, add all points of all measures that apply to the danger scenarios

        $dangers = Danger::all();

        $labels = [];
        $values = [];

        foreach ($dangers as $danger) {
            /*
             * @var Danger $danger
             */
            $labels[] = $danger->name;

            $v = $danger->scenarios->reduce(function ($acc, Scenario $scenario) use ($measureLevels) {
                $exposition = $this->getFutureExpositionPointsForScenario($scenario, $measureLevels);

                return $acc + $exposition;
            }, 0);

            $values[] = max(10, $v / $danger->scenarios->count());
        }

        return [
            'labels' => $labels,
            'data' => $values,
        ];
    }

    public function getAttackDataForEvaluation(Evaluation $evaluation): array
    {
        $scenarios = Scenario::with('measures')->get();

        $measureLevels = collect($evaluation->measureLevels);

        // For each scenario, add all points of all selected measure levels and divide by number of measures
        // (fundamental measures count double)

        $values = $scenarios->map(function (Scenario $scenario) use ($measureLevels) {
            return max(10, $this->getExpositionPointsForScenario($scenario, $measureLevels));
        });

        return [
            'labels' => $scenarios->pluck('name'),
            'data' => $values,
        ];
    }

    public function getMaturityDataForEvaluation(Evaluation $evaluation): array
    {
        // Calculate automatic answer results
        $evaluation->saveAutomaticAnswers();

        $evaluation = Evaluation::find($evaluation->id);

        $maturityLevels = $evaluation->maturityLevels;

        $values = $maturityLevels->map(function (EvaluationMaturityAnswer $ml) {
            return ['level' => $ml->answer->level, 'label' => $ml->question->title];
        });

        return [
            'labels' => $values->pluck('label'),
            'data' => $values->pluck('level'),
        ];
    }

    public function getBestMeasuresForEvaluation(Evaluation $evaluation): array
    {
        $measureLevels = collect($evaluation->measureLevels);

        // For each danger, add all points of all measures that apply to the danger scenarios

        $measures = Measure::all();

        Log::info('____ Calcul des meilleures mesures pour l\'évaluation ' . $evaluation->id . ' ____');
        $bestMeasures = array_map(function ($i) {return 0; }, array_flip($measures->pluck('id')->toArray()));

        $res = DB::select('select measures.id, count(*) as qty from measures
join measure_scenario on measures.id=measure_scenario.measure_id
join danger_scenario on danger_scenario.scenario_id=measure_scenario.scenario_id
group by measures.id');

        $measureCount = [];

        foreach ($res as $row) {
            $measureCount[$row->id] = $row->qty;
        }

        /**
         * @var DangerLevelEvaluation $dangerLevel
         */
        // Nous regardons chaque niveau de danger choisi à l'étape 1 pour cette évaluation
        foreach ($evaluation->dangerLevels as $dangerLevel) {
            Log::info('*** Danger ' . $dangerLevel->danger->name . ' ***');
            Log::info('Ce danger appartient à ' . count($dangerLevel->danger->scenarios) . ' scenarios');
            /**
             * @var Scenario $scenario
             */
            // Pour chaque niveau de danger, nous regardons les scénarios qui sont liés au danger
            foreach ($dangerLevel->danger->scenarios as $scenario) {
                Log::info('--- Scenario ' . $scenario->name . ' ---');
                Log::info('Ce scenario contient ' . count($scenario->measures) . ' mesures');
                // Add points for this measure if there is a difference in exposition

                // Pour chaque scenarios nous regardons les mesures qui l'affectent
                foreach ($scenario->measures as $measure) {
                    Log::info('- Mesure ' . $measure->short_name . ($measure->fundamental ? '(fondamentale) -' : ' -'));
                    /**
                     * @var MeasureLevel|null $selectedLevel
                     */
                    // Nous regardons quel niveau a été choisit à l'étape 2 pour cette mesure.
                    $selectedLevel = $measureLevels->first(function (MeasureLevel $level) use ($measure) {
                        return $measure->id === $level->measure_id;
                    });
                    Log::info('Niveau actuel de la mesure : ' . $selectedLevel->actual_level);

                    if (!$selectedLevel) {
                        // Si aucun niveau n'a été choisi pour cette mesure, nous lui mettons le niveau 0
                        // Cela ne devrait pas être possible car le choix d'un niveau est obligatoire à l'étape 2
                        // Toutefois, mieux vaut s'assurer de sa présence pour éviter un crash
                        $selectedLevel = new MeasureLevel();
                        $selectedLevel->actual_level = 0;
                    }

                    // La valeur du niveau actuel choisit pour cette mesure
                    // (les mesures fondamentales sont déja multipliées par deux en base de données)
                    $l1 = $measure->{'level' . $selectedLevel->actual_level . '_value'};
                    Log::info('Valeur du niveau actuel : ' . $measure->{'level' . $selectedLevel->actual_level . '_value'});
                    // La valeur du niveau juste au dessus du niveau actuel choisit pour cette mesure (avec un maximum de niveau 3)
                    // (les mesures fondamentales sont déja multipliées par deux en base de données)
                    $l2 = $measure->{'level' . min($selectedLevel->actual_level + 1, 3) . '_value'};
                    Log::info('Valeur du niveau actuel + 1 : ' . $measure->{'level' . min($selectedLevel->actual_level + 1, 3) . '_value'});

                    // La différence entre la valeur du niveau N + 1 et du niveau N (progression potentielle)
                    // est pondérée par la perception du danger

                    // puis ajouté au score pour cette mesure
                    $ponderatedIncrement = ($l2 - $l1) * $dangerLevel->level->value / $measureCount[$measure->id];
                    $bestMeasures[$measure->id] += $ponderatedIncrement;
                    Log::info('Incrément pondéré : ' . $ponderatedIncrement);
                    Log::info('Valeur courante de la mesure ' . $measure->id . ' (' . $measure->short_name . ') : ' . $bestMeasures[$measure->id]);
                }
            }
        }
        foreach ($bestMeasures as $k => $m) {
            Log::info('Valeur finale de la mesure ' . $k . ' : ' . $m);
        }

        // Les mesures sont mises dans l'ordre du plus grand score au plus petit et renvoyées.

        return collect($bestMeasures)->sortDesc()->keys()->toArray();
    }

    private function getExpositionPointsForScenario(Scenario $scenario, $measureLevels): float
    {
        // Get prorated count of measures
        $count = $scenario->measures->reduce(function ($acc, Measure $measure) {
            if ($measure->fundamental) {
                return $acc + 2;
            }

            return $acc + 1;
        }, 0);
        // for each measure in this scenario, add points for the level that is selected
        $points = $scenario->measures->reduce(function ($acc, Measure $measure) use ($measureLevels) {
            $selectedLevel = $measureLevels->first(function (MeasureLevel $level) use ($measure) {
                return $measure->id === $level->measure_id;
            });
            if (!$selectedLevel) {
                return $acc;
            }
            // fundamental measures count double
            return $acc + $measure->{'level' . $selectedLevel->actual_level . '_value'};
        }, 0);

        return floor(100 - ($points / $count));
    }

    private function getFutureExpositionPointsForScenario(Scenario $scenario, $measureLevels): float
    {
        // Get prorated count of measures
        $count = $scenario->measures->reduce(function ($acc, Measure $measure) {
            if ($measure->fundamental) {
                return $acc + 2;
            }

            return $acc + 1;
        }, 0);
        // for each measure in this scenario, add points for the level that is selected
        $points = $scenario->measures->reduce(function ($acc, Measure $measure) use ($measureLevels) {
            $selectedLevel = $measureLevels->first(function (MeasureLevel $level) use ($measure) {
                return $measure->id === $level->measure_id;
            });

            // fundamental measures count double
            $val = $measure->{'level' . $selectedLevel->expected_level . '_value'};
            if (!$val) {
                $val = $measure->{'level' . $selectedLevel->actual_level . '_value'};
            }

            return $acc + $val;
        }, 0);

        return floor(100 - ($points / $count));
    }

    public function getActionTerritoryGraphData(): array
    {
        // Pour cq territoire
        // trouver les evals des structures
        // sort les evals pour recup currentEvaluation & previousEvaluation
        // puis plannedMeasures par structure
        // puis doneMeasure par structure
        // puis count par territoire

        $territories = Territory::with([
            'organizations',
            'organizations.evaluations',
            'organizations.evaluations.measureLevels',
        ])->get();

        $labels = [];
        $plannedMeasures = [];
        $doneMeasures = [];

        foreach ($territories as $territory) {
            /*
             * @var Territory $territory
             */
            $labels[] = $territory->name;

            $plannedMeasures[] = $territory->organizations->filter(function (Organization $org) {
                return $org->active;
            })->reduce(function ($acc, Organization $org) {
                $doneEvaluations = $org->doneEvaluations;
                if ($doneEvaluations->count() >= 2) {
                    /**
                     * @var Evaluation $currentEval
                     */
                    $currentEval = $doneEvaluations[0];

                    // Get the number of planned actions in the current evaluation
                    return $acc + $currentEval->measureLevels->filter(function (MeasureLevel $ml) {
                        return null !== $ml->expected_level && 0 !== $ml->expected_level;
                    })->count();
                }

                return $acc;
            }, 0);

            $doneMeasures[] = $territory->organizations->filter(function (Organization $org) {
                return $org->active;
            })->reduce(function ($acc, Organization $org) {
                $doneEvaluations = $org->doneEvaluations;
                if ($doneEvaluations->count() >= 2) {
                    /**
                     * @var Evaluation $currentEval
                     */
                    $currentEval = $doneEvaluations[0];
                    /**
                     * @var Evaluation $previousEval
                     */
                    $previousEval = $doneEvaluations[1];

                    // Get the number of planned actions in the previous evaluation
                    // that have reached the expected_level in the current evaluation
                    return $acc + $currentEval->measureLevels->filter(function (MeasureLevel $ml) use ($previousEval) {
                        $prevML = $previousEval->measureLevels->first(function (MeasureLevel $pml) use ($ml) {
                            return $ml->measure_id === $pml->measure_id;
                        });

                        return $prevML && $ml->actual_level >= $prevML->expected_level;
                    })->count();
                }

                return $acc;
            }, 0);
        }

        return [
            'labels' => $labels,
            'data' => [$plannedMeasures, $doneMeasures],
        ];
    }

    public function getOrganizationData(int|null $territory_id = null)
    {
        $orgQuery = Organization::where('active', true);

        if ($territory_id) {
            $orgQuery->where('territory_id', $territory_id);
        }

        $orgs = $orgQuery->get();

        $count = $orgs->count();

        if (0 === $count) {
            return [
                'maturity' => 0,        // Nombre de structures évaluées
                'count' => 0,          // Indice de maturité moyen des structures actives
                'top_measures' => [],                // Top 3 des niveaux de mesures planifiées (doit retourner le nom court de la mesure, le niveau et le nombre de structures concernées / ou directement le %)
            ];
        }

        $meanMaturity = $orgs->reduce(function ($acc, Organization $organization) {
            /**
             * @var Evaluation|null $doneEvaluation
             */
            $doneEvaluation = $organization->doneEvaluations->first();

            if (!$doneEvaluation) {
                return $acc;
            }

            return $acc + $doneEvaluation->getMaturityCyberAttribute();
        }, 0) / $count;

        $topMeasures = [];

        foreach ($orgs as $org) {
            /**
             * @var Evaluation|null $currentEvaluation
             */
            $currentEvaluation = $org->doneEvaluations->first();

            if (!$currentEvaluation) {
                continue;
            }

            foreach ($currentEvaluation->measureLevels as $level) {
                if (!$level->expected_level) {
                    continue;
                }
                if (array_key_exists($level->measure->short_name, $topMeasures)) {
                    ++$topMeasures[$level->measure->short_name]['level' . $level->expected_level];
                    ++$topMeasures[$level->measure->short_name]['organizations'];
                } else {
                    $topMeasures[$level->measure->short_name] = [
                        'short_name' => $level->measure->short_name,
                        'level1' => 1 === $level->expected_level ? 1 : 0,
                        'level2' => 2 === $level->expected_level ? 1 : 0,
                        'level3' => 3 === $level->expected_level ? 1 : 0,
                        'organizations' => 1,
                    ];
                }
            }
        }

        $topMeasures = collect($topMeasures)->map(function ($topMeasure) use ($count) {
            $levelValues = [
                1 => $topMeasure['level1'],
                2 => $topMeasure['level2'],
                3 => $topMeasure['level3'],
            ];

            $maxValue = max($levelValues);

            $maxLevels = array_keys($levelValues, $maxValue);

            return [
                'short_name' => $topMeasure['short_name'],
                'organizations' => $topMeasure['organizations'],
                'max_levels' => collect($maxLevels)->sortDesc()->toArray(),
                'max_value' => $maxValue,
                'percentage' => 100 * $maxValue / $count,
            ];
        })->values()->sortBy('max_value', SORT_NUMERIC, true)->splice(0, 3)->toArray();

        return [
            'maturity' => $meanMaturity,        // Nombre de structures évaluées
            'count' => $count,          // Indice de maturité moyen des structures actives
            'top_measures' => $topMeasures,                // Top 3 des niveaux de mesures planifiées (doit retourner le nom court de la mesure, le niveau et le nombre de structures concernées / ou directement le %)
        ];
    }

    public static function hue2rgb($p, $q, $t)
    {
        if ($t < 0) {
            ++$t;
        }
        if ($t > 1) {
            --$t;
        }
        if ($t < 1 / 6) {
            return $p + ($q - $p) * 6 * $t;
        }
        if ($t < 1 / 2) {
            return $q;
        }
        if ($t < 2 / 3) {
            return $p + ($q - $p) * (2 / 3 - $t) * 6;
        }

        return $p;
    }

    public static function hslToRgb($h, $s, $l)
    {
        if (0 == $s) {
            $r = $l;
            $g = $l;
            $b = $l; // achromatic
        } else {
            $q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
            $p = 2 * $l - $q;
            $r = self::hue2rgb($p, $q, $h + 1 / 3);
            $g = self::hue2rgb($p, $q, $h);
            $b = self::hue2rgb($p, $q, $h - 1 / 3);
        }

        return [round($r * 255), round($g * 255), round($b * 255)];
    }
}