Der Cyber Resiliance Act
Der Cyber Resiliance Act (CRA) ist eine EU-Verordnung für Mindestanforderungen an vernetzte Produkte mit Softwareanteil, die darauf abzielt diese Art von Produkten widerstandsfähig gegen zum Beispiel Cyberangriffe zu machen. Der CRA wurde am 23. Oktober 2024 verabschiedet und die darin definierten Anforderungen müssen schrittweise immer weiter erfüllt werden. Ende 2027 müssen spätestens alle Anforderungen bei neuen Produkten eingehalten werden. Abbildung 1 zeigt die Meilensteine bei der Umsetzung des CRA.

Schwachstellen in Softwarekomponenten müssen auffindbar sein
Eine wichtige Anforderung des CRA ist es, dass Schwachstellen in Softwarekomponenten entdeckt und bei Bedarf behoben werden können. Dafür muss bekannt sein aus welchen Komponenten eine Softwareanwendung besteht bzw. von welchen Komponenten die Anwendung abhängig ist. Es existieren bereits heute eine Reihe von so genannten Sicherheitslücken-Datenbanken (vulnerability data base). Diese enthalten Informationen über bereits bekannte Sicherheitslücken in Softwarekomponenten, die möglicherweise einen Cyberangriff begünstigen. Eine Softwareanwendung, die eine Komponente mit einer bekannten Sicherheitslücke verwendet gilt dann auch als potenziell gefährdet.
Softwarestücklisten unterstützen die Sicherheitsanalysen
Um die Schwachstellenanalyse automatisiert durchzuführen gibt es den Ansatz in Zusammenhang mit der Erfüllung der Anforderungen das CRA eine Softwarestückliste (SBOM) zu erstellen und mit dem Produkt mitzuliefern. Diese Softwarestückliste enthält möglichst alle Informationen über die im Produkt eingesetzten Softwarekomponenten und deren Version. Solch eine SBOM liegt dann als digitalisierte Datei in standardisierten Formaten vor und kann damit von Analysewerkzeugen automatisch verarbeitet und analysiert werden. Ein bekanntes Format für eine SBOM ist zum Beispiel das CycloneDX-Format (https://cyclonedx.org/specification/overview/).
Daten aus bestehenden (Windows-)Anwendungen generieren
Viele Softwareanwendungen existieren bereits und eine SBOM ist nicht vorhanden. Oftmals ist das Einzige, was zur Verfügung steht die installierte Anwendung selbst.
Wenn es sich um eine Windows-Anwendung handelt, dann besteht diese aus ausführbaren Dateien (EXE = executable) und genutzen Bibliotheken (DLL = Dynamic Linked Library). Schaut man sich die Eigenschaften einer solchen Datei mit Windowss-Boardmitteln an, dann sieht man, dass in einer solchen Datei eine Reihe von Informationen stecken, die hilfreich für die Erstellung einer SBOM sein könnten (Abbildung 2).

Auslesen der Dateieigenschaften durch C#-Code
Es kam nun die Idee auf, die Dateieigenschaften der EXE- und DLL-Dateien mit einem selbstgeschriebenen Programm auszulesen und abzuspeichern. Die Basis für eine Umsetzung war ein Eintrag auf Stack Overflow: https://stackoverflow.com/questions/7861886/how-to-get-file-properties
Zusätzlich zu dieser FileInfo gibt es bei mit .NET erstellten Komponenten außerdem noch zusätzliche Informationen wenn es sich um so genannte .NET-Assemblies handelt. Diese können auch noch ausgelesen und verarbeitet werden.
Der Code zur Analyse der Softwarekomponenten stellt sich folgendermaßen dar:
|
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 |
public List<SoftwareComponent> Analyze(string folderPath) { _folderPath = folderPath; List<SoftwareComponent> result = new List<SoftwareComponent>(); DirectoryInfo directoryInfo = new DirectoryInfo(folderPath); // create a list of all EXE and DLL files in the given folder FileInfo[] exeFileInfos = directoryInfo.GetFiles("*.exe"); FileInfo[] dllFileInfos = directoryInfo.GetFiles("*.dll"); List<FileInfo> fileInfos = new List<FileInfo>(); fileInfos.AddRange(exeFileInfos); fileInfos.AddRange(dllFileInfos); // iterate over all files in the list foreach (FileInfo fileInfo in fileInfos) { // Get the file version. FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(fileInfo.FullName); SoftwareComponent mainComponent = new SoftwareComponent() { FileDescription = fileVersionInfo.FileDescription, OriginalFileName = fileInfo.Name, FileVersion = fileVersionInfo.FileVersion, ProductName = fileVersionInfo.ProductName, ProductVersion = fileVersionInfo.ProductVersion, LastModified = fileInfo.LastWriteTime, SoftwareComponentType = ESoftwareComponentType.WindowsDLL }; if(fileInfo.Extension.ToLower() == ".exe") { mainComponent.SoftwareComponentType = ESoftwareComponentType.WindowsExecutable; } result.Add(mainComponent); // now we check if we can get info about a .NET assembly - else we get an exception try { List<string> assemblyPaths = new List<string>(); assemblyPaths.Add(fileInfo.FullName); PathAssemblyResolver pathAssemblyResolver = new PathAssemblyResolver(assemblyPaths); MetadataLoadContext metadataLoadContext = new MetadataLoadContext(pathAssemblyResolver); Assembly assembly = metadataLoadContext.LoadFromAssemblyPath(fileInfo.FullName); if (assembly != null) { mainComponent.AssemblyName = assembly.GetName().Name; mainComponent.AssemblyVersion = assembly.GetName().Version.ToString(); mainComponent.SoftwareComponentType = ESoftwareComponentType.DotNetAssembly; FillDependenciesRecursively(assembly, mainComponent); } } catch (Exception exception) { //Console.WriteLine("NO .NET ASSEMBLY " + fileInfo.Name); } } return result; } /// <summary> /// Create information about the referenced assembies /// </summary> /// <param name="currentAssembly">The current .NET assembly</param> /// <param name="currentComponent">The current SoftwareComponent data object</param> private void FillDependenciesRecursively(Assembly currentAssembly, SoftwareComponent currentComponent) { AssemblyName[] referencedAssemblies = currentAssembly.GetReferencedAssemblies(); foreach (AssemblyName assemblyName in referencedAssemblies) { SoftwareComponent subComponent = new SoftwareComponent { AssemblyVersion = assemblyName.Version.ToString(), AssemblyName = assemblyName.Name, SoftwareComponentType = ESoftwareComponentType.DotNetAssembly, OriginalFileName = assemblyName.CodeBase }; currentComponent.Dependencies.Add(subComponent); //try //{ // Assembly subAssembly = Assembly.LoadFile(_folderPath + "/" + assemblyName.Name + ".dll"); // FillDependenciesRecursively(subAssembly, subComponent); //} //catch(Exception exception) //{ // Debug.WriteLine("***" + exception.Message); //} } } |
In der Hilfsmethode FillDependenciesRecursively wird aktuell bewusst auf den rekursiven Aufruf verzichtet, da normalerweise alle Bibliotheken, die zum Funktionieren einer Anwendung notwendig sind im Ordner der Anwendung enthalten sind und sowieso alle Dateien in diesem Ordner berücksichtigt werden – mal abgesehen von Bibliotheken an globaler Stelle. Für deren Sicherheit ist aber normalerweise z.B. der Hersteller Microsoft verantwortlich.
Ein kleine Benutzerschnittstelle erleichtert den Einsatz der Software
Um den Code möglichst einfach nutzbar zu machen, habe ich eine kleine grafische Benutzeroberfläche mit Windows Forms realisiert. Abbildung 3 zeigt die Anwendung. Hier lässt sich der Ordner auswählen, in dem sich die Anwendungsdateien befinden. Außerdem der Zielordner für die Analyseergebnisse.

Die Anwendung ist als Open-Source-Software über GitHub zu erhalten. Wenn man einen GitHub-Zugang hat, kann man diese als Ergebnis eines Build-Skripts hier herunterladen, entpacken und starten: https://github.com/oalt/MDD4All.DependencyAnalysis.Apps.WinForms/actions
Weitere Infos findet man auch auf der Readme-Seite des Projekts: https://github.com/oalt/MDD4All.DependencyAnalysis.Apps.WinForms