|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Audit Preparation in E-Rezept App (iOS)" |
| 4 | +date: 2025-05-06 14:00:00 +0200 |
| 5 | +author: Martin Fiebig |
| 6 | +categories: tech |
| 7 | +tags: Audits iOS |
| 8 | +excerpt: <br />Our technical approach for preparing for audits explained.<br /><br /> |
| 9 | +--- |
| 10 | + |
| 11 | +<div class="note"> |
| 12 | +<p> |
| 13 | + This is the continuation of a previous article <a href="/tech/2025/01/21/audit-preparation">Audit Preparation in E-Rezept App (Android)</a>. If you are interested in how we prepare audits for our Android E-Rezept App, feel free to read that article first. |
| 14 | +</p> |
| 15 | +</div> |
| 16 | + |
| 17 | + |
| 18 | +# Preparing for Audits in the E-Rezept App iOS |
| 19 | + |
| 20 | +The iOS Application is subject to audits much like the Android application. Our application is mainly written using Swift. At the point of writing this article, Swift does not support annotations as they can be used with Gradle plugins, so there is no formal way of annotating code as we would need. |
| 21 | + |
| 22 | +In this article we will present a distinct approach to annotate the code differently from Android and walk through the steps of combining requirements with code to form an output document to help with auditing the code. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Requirements |
| 27 | + |
| 28 | +In order to deliver a secure product we have to implement various formal requirements. These requirements are published regularly for all gematik products. You can find them on our website *gemSpecPages* ([https://gemspec.gematik.de](https://gemspec.gematik.de)). An example (and our *main*) specification for the E-Rezept App is called `gemSpec_eRp_FdV`. You can find the latest version of the actual specification under [https://gemspec.gematik.de/docs/gemSpec/gemSpec_eRp_FdV/latest/](https://gemspec.gematik.de/docs/gemSpec/gemSpec_eRp_FdV/latest/). |
| 29 | + |
| 30 | +While preparing the document for our auditors, we will use gemspec pages as a source for the specification text, as well as another specification that is provided by the BSI. The BSI specification is published as a PDF document, to use it within the scripts, we transformed parts of that PDF into an HTML document with the same structure as gemspecPages. |
| 31 | + |
| 32 | +## Annotations |
| 33 | + |
| 34 | +Auditors expect an explanation of how and where a specific requirement is implemented. For that purpose, we use special annotations within our code for each requirement as well as a text document for more general comments that do not fit to any parts of the code. The special annotations are simple comments within the code, that flag a line or a region of code to be important for a specific requirement. |
| 35 | + |
| 36 | +The format of the annotation looks like this: |
| 37 | + |
| 38 | +``` |
| 39 | + [REQ:<SPEC>:<AFO>#<ORDER>|<LOC_TO_INCLUDE>] <NOTE> |
| 40 | +``` |
| 41 | + |
| 42 | +Annotations are composed with these parts: |
| 43 | + |
| 44 | + - `[REQ:`: marks the start of a requirement annotation. |
| 45 | + - `<SPEC>`: Technical name of the specification (document name), that originates the requirement. |
| 46 | + - `:<AFO>`: Static `:` followed by the identifier of the implemented requirement. |
| 47 | + - `#<ORDER>` (optional): Static `#` followed by a number. All references of a single requirement are ordered by this number in the document. This allows a deterministic order of all code parts and makes the explanation easier to understand. |
| 48 | + - `|<LOC_TO_INCLUDE>` (optional): The number of lines that the annotation is meant for. Defaults to 1. |
| 49 | + - `] `: Marks the end of the technical part of the annotation. |
| 50 | + - `<NOTE>` (optional): Textual explanation of the implementation or why this part is important for the requirement. |
| 51 | + |
| 52 | +### Example |
| 53 | + |
| 54 | +Looking at [DefaultIDPSession.swift#L196](https://github.com/gematik/E-Rezept-App-iOS/blob/master/Sources/IDP/DefaultIDPSession.swift#L196) we can see the annotation: |
| 55 | + |
| 56 | +``` |
| 57 | +[REQ:gemSpec_IDP_Frontend:A_21323,A_21324#1|6] Crypto box contains `Token-Key` |
| 58 | +``` |
| 59 | + |
| 60 | +containing the following information: |
| 61 | + |
| 62 | + - `gemSpec_IDP_Frontend`: The specification document where the requirements can be found |
| 63 | + - `A_21323,A_21324#1`: The part of the code spans two requirements, one being `A_21323` the other being `A_21324`. The annotation is ranked #1 for the latter. |
| 64 | + - `|6`: The annotation is meant for the following 6 lines of code. |
| 65 | + |
| 66 | +## Document generation |
| 67 | + |
| 68 | +The actual generation of the document is written in Ruby, called by the `list_requirements` lane of our [Fastfile](https://github.com/gematik/E-Rezept-App-iOS/blob/master/fastlane/Fastfile). We extracted the actual code into a fastlane plugin to clean up the fastfile. You can find the plugin on Github [gematik/Fastlane-Plugin-CI-CD](https://github.com/gematik/Fastlane-Plugin-CI-CD). The action that generates the audit document is named `audit_generator`. |
| 69 | + |
| 70 | +### Processing Annotations |
| 71 | + |
| 72 | +Let's have a look into that plugin, to see how this works [audit_generator.rb#L15](https://github.com/gematik/Fastlane-Plugin-CI-CD/blob/main/lib/fastlane/plugin/ci_cd/actions/audit_generator.rb#L15): |
| 73 | + |
| 74 | +```ruby |
| 75 | +def self.run(params) |
| 76 | + UI.message("The audit_generator plugin is working!") |
| 77 | + |
| 78 | + # Load data and initialize processors |
| 79 | + data_loader = Fastlane::Helper::AuditDataLoader.new(params[:audit_afos_json]) |
| 80 | + file_processor = Fastlane::Helper::FileProcessor.new |
| 81 | + spec_fetcher = Fastlane::Helper::SpecFetcher.new(data_loader.audit_afos_json) |
| 82 | + |
| 83 | + # Process requirement notes if provided |
| 84 | + if params[:requirement_notes_glob] |
| 85 | + UI.message("Processing requirement notes from: #{params[:requirement_notes_glob]}") |
| 86 | + Dir.glob(params[:requirement_notes_glob]).each do |file| |
| 87 | + file_processor.process_requirement_notes(file) |
| 88 | + end |
| 89 | + end |
| 90 | + |
| 91 | + # Process code files |
| 92 | + source_files = [] |
| 93 | + params[:source_file_globs].each do |glob| |
| 94 | + UI.message("Adding source files from glob: #{glob}") |
| 95 | + source_files.concat(Dir.glob(glob)) |
| 96 | + end |
| 97 | + |
| 98 | + source_files.each do |file| |
| 99 | + file_processor.process_code_file(file) |
| 100 | + end |
| 101 | + |
| 102 | + # Get specs and prepare data for templates |
| 103 | + original_texts = spec_fetcher.fetch_specs |
| 104 | + audit_data = prepare_audit_data(data_loader.audit_afos, file_processor.specs, original_texts) |
| 105 | + |
| 106 | + # Generate output files |
| 107 | + generate_output_files(params, audit_data) |
| 108 | +end |
| 109 | +``` |
| 110 | + |
| 111 | +After some basic initialization the process consists of 4 steps: |
| 112 | + |
| 113 | + 1. Parsing the requirement notes document (if present) containing general information on some of the implemented requirements |
| 114 | + 2. Scanning all given source files for requirement annotations |
| 115 | + 3. Processing |
| 116 | + 1. Download the original specifications |
| 117 | + 2. Processing all found annotation information, grouping by spec, sorting |
| 118 | + 4. Rendering the information into all given templates |
| 119 | + |
| 120 | +#### Parsing requirement notes & scanning all given source files |
| 121 | + |
| 122 | +The actual parsing of the requirement notes document as well as the given source files is similar. We identify blocks that contain annotations. While in the requirement notes each block will consist of one line in the document, the source files are scanned for comment sections. Subsequent commented lines are treated as one block. |
| 123 | + |
| 124 | +Each block is then parsed using a regular expression: |
| 125 | + |
| 126 | +```ruby |
| 127 | +block.each do |line| |
| 128 | + # Format: [REQ:<SPEC>:<AFOS>:<SUBAFOS?>] <DESC> |
| 129 | + if (matches = line.match(/\[REQ:(?<SPEC>[^:]*):(?<AFOS>[^:^|\s]*)(?:(?::)(?<SUBAFOS>[^:^|\s]*))?(?:(?:\|)(?<NUMBEROFLINES>[^:\s]*))?\](?<DESC>.*)/)) |
| 130 | + register(matches, file, number, trimmed_code, source, trimming) |
| 131 | + end |
| 132 | + number += 1 |
| 133 | +end |
| 134 | +``` |
| 135 | + |
| 136 | +The `register(...)` method then gathers all the information in a structured way. |
| 137 | + |
| 138 | +#### Processing |
| 139 | + |
| 140 | +The processing starts by downloading all original requirements from the specifications listed in the `audit_afos.json` file. This allows us to include the official requirement text alongside our implementation details. |
| 141 | + |
| 142 | +##### Original Requirement Texts |
| 143 | + |
| 144 | +The `audit_afos.json` file serves as a central registry of all requirements (AFOs) that will be checked by the auditors, as well as a list of specifications that are used. Here's an example of what this file might look like: |
| 145 | + |
| 146 | +```json |
| 147 | +{ |
| 148 | + "audit_afos": [ |
| 149 | + "A_20742", "A_20743", "A_20744", "A_21323", "A_21324" |
| 150 | + ], |
| 151 | + "specs": { |
| 152 | + "gemSpec_eRp_FdV": "https://gemspec.gematik.de/docs/gemSpec/gemSpec_eRp_FdV/latest/", |
| 153 | + "gemSpec_IDP_Frontend": "https://gemspec.gematik.de/docs/gemSpec/gemSpec_IDP_Frontend/latest/", |
| 154 | + "BSI_eRp": "file:///path/to/local/bsi_requirements.html" |
| 155 | + } |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +For each specification, we download and parse the HTML content using the `Nokogiri` gem. Since each requirement within the HTML document is identified by a unique ID, we can extract the original requirement text and include it in our audit document: |
| 160 | + |
| 161 | +```ruby |
| 162 | +def fetch_requirement_text(spec_doc, afo) |
| 163 | + # Find the element with the AFO ID |
| 164 | + element = spec_doc.css("##{afo}") |
| 165 | + return "Original text not found" if element.empty? |
| 166 | + |
| 167 | + # Extract the requirement text |
| 168 | + element.inner_html |
| 169 | +end |
| 170 | +``` |
| 171 | + |
| 172 | +For BSI requirements, which are typically provided as PDF documents, we convert the relevant sections to HTML with the same structure as gemspecPages to maintain consistency in our processing pipeline. |
| 173 | + |
| 174 | +##### Filtering and Organization |
| 175 | + |
| 176 | +Before generating the final output, we apply several filters and organizational steps to ensure the audit document is comprehensive and easy to navigate: |
| 177 | + |
| 178 | + 1. **Completeness Check:** We identify any requirements from the `audit_afos.json` file that don't have corresponding implementations, marking them as "missing" in the output. |
| 179 | + 2. **Grouping by Specification:** All implementations are grouped by their source specification, making it easier for auditors to verify compliance with specific documents. |
| 180 | + 3. **Sorting:** Within each specification, implementations are sorted by AFO ID and then by the optional order parameter, ensuring a logical and consistent presentation. |
| 181 | + 4. **Deduplication:** If the same code section is referenced multiple times for the same requirement, we consolidate these references to avoid redundancy. |
| 182 | + |
| 183 | +#### Output Generation |
| 184 | + |
| 185 | +The final step is generating the output files based on the processed data and the provided ERB templates: |
| 186 | + |
| 187 | +```ruby |
| 188 | +def generate_output_files(params, audit_data) |
| 189 | + FileUtils.mkdir_p(params[:output_directory]) |
| 190 | + |
| 191 | + params[:erb_templates].each do |template_path| |
| 192 | + template_name = File.basename(template_path, '.erb') |
| 193 | + output_path = File.join(params[:output_directory], template_name) |
| 194 | + |
| 195 | + UI.message("Generating output file: #{output_path}") |
| 196 | + |
| 197 | + # Load and render the ERB template |
| 198 | + template = ERB.new(File.read(template_path)) |
| 199 | + result = template.result_with_hash( |
| 200 | + specs: audit_data[:specs], |
| 201 | + missing_afos: audit_data[:missing_afos], |
| 202 | + original_texts: audit_data[:original_texts] |
| 203 | + ) |
| 204 | + |
| 205 | + # Write the output file |
| 206 | + File.write(output_path, result) |
| 207 | + end |
| 208 | +end |
| 209 | +``` |
| 210 | + |
| 211 | +By using ERB templates we support multiple output formats. This flexibility allows us to generate: |
| 212 | + |
| 213 | + 1. **Markdown** documents for easy reading and version control |
| 214 | + 2. **HTML** reports with interactive features for easier navigation |
| 215 | + 3. **PDF** documents (via HTML or Markdown + `pandoc` CLI tool) for formal submission to auditors |
| 216 | + |
| 217 | +### Conclusion |
| 218 | + |
| 219 | +By using this systematic approach to documenting our implementation of security requirements, we've significantly streamlined the audit preparation process for the iOS E-Rezept App. The combination of code annotations, automated document generation, and integration with our CI/CD pipeline ensures that: |
| 220 | + |
| 221 | +1. Developers clearly document how they've implemented each requirement |
| 222 | +2. Auditors can easily trace requirements to their implementations |
| 223 | +3. Missing implementations are quickly identified and addressed |
| 224 | +4. The documentation stays synchronized with the codebase |
| 225 | + |
| 226 | +This approach has not only made the audit process more efficient but has also improved the overall quality of our codebase by encouraging developers to think explicitly about security requirements as they write code. |
| 227 | + |
| 228 | +The fastlane plugin we've developed for this purpose is open-source and available on GitHub, so other teams facing similar audit requirements can benefit from our approach. |
| 229 | + |
| 230 | +--- |
| 231 | + |
| 232 | +# About the author |
| 233 | + |
| 234 | +Martin Fiebig has worked as a software engineer for 21 years, 13 years on iOS and Apple platforms. His focus topics include large scale mobile applications and developer experience. Martin leads the iOS team at gematik since 2020 and the Mobile Chapter since 2023. |
0 commit comments