Skip to content

Commit

Permalink
Add support for user config file for plugin options (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
nightlark authored Aug 7, 2024
1 parent a3785e1 commit d98377e
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 11 deletions.
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,49 @@ pip install -e ".[test,dev]"
pip install -e plugins/fuzzyhashes
```

## Settings

Surfactant settings can be changed using the `surfactant config` subcommand, or by hand editing the settings configuration file (this is not the same as the JSON file used to configure settings for a particular sample that is described later).

### Command Line

Using `surfactant config` is very similar to the basic use of `git config`. The key whose value is being accessed will be in the form `section.option` where `section` is typically a plugin name or `core`, and `option` is the option to set. As an example, the `core.recorded_institution` option can be used to configure the recorded institution used to identify who the creator of a generated SBOM was.

Setting this option to `LLNL` could be done with the following command:

```bash
surfactant config core.recorded_institution LLNL
```

Getting the currently set value for the option would then be done with:

```bash
surfactant config core.recorded_institution
```

### Manual Editing

If desired, the settings config file can also be manually edited. The location of the file will depend on your platform.
On Unix-like platforms (including macOS), the XDG directory specification is followed and settings will be stored in
`${XDG_CONFIG_HOME}/surfactant/config.toml`. If the `XDG_CONFIG_HOME` environment variable is not set, the location defaults
to `~/.config`. On Windows, the file is stored in the Roaming AppData folder at `%APPDATA%\\surfactant\\config.toml`.

The file itself is a TOML file, and for the previously mentioned example plugin may look something like this:

```toml
[core]
recorded_institution = "LLNL"
```

## Usage

### Identify sample file

In order to test out surfactant, you will need a sample file/folder. If you don't have one on hand, you can download and use the portable .zip file from <https://github.com/ShareX/ShareX/releases> or the Linux .tar.gz file from <https://github.com/GMLC-TDC/HELICS/releases>. Alternatively, you can pick a sample from https://lc.llnl.gov/gitlab/cir-software-assurance/unpacker-to-sbom-test-files

### Build configuration file
### Build configuration file for sample

A configuration file contains the information about the sample to gather information from. Example JSON configuration files can be found in the examples folder of this repository.
A configuration file for a sample contains the information about the sample to gather information from. Example JSON sample configuration files can be found in the examples folder of this repository.

**extractPaths**: (required) the absolute path or relative path from location of current working directory that `surfactant` is being run from to the sample folders, cannot be a file (Note that even on Windows, Unix style `/` directory separators should be used in paths)\
**archive**: (optional) the full path, including file name, of the zip, exe installer, or other archive file that the folders in **extractPaths** were extracted from. This is used to collect metadata about the overall sample and will be added as a "Contains" relationship to all software entries found in the various **extractPaths**\
Expand Down
95 changes: 95 additions & 0 deletions docs/api/config_manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# ConfigManager

The `ConfigManager` class is used to handle settings stored in a configuration file. It supports reading and writing configuration values while preserving formatting and comments, and it caches the configuration to avoid reloading it multiple times during the application's runtime. The underlying config file location is dependent on the operating system, though typically follows the XDG directory specification and respects the `XDG_CONFIG_HOME` environment variable on Unix-like platforms. On Windows, the configuration file is stored in the AppData Roaming folder (`%APPDATA%`).

## Usage

## Configuration File Location

The location of the configuration file varies depending on the platform:

- **Windows**: `%AppData%\surfactant\config.toml`
- **macOS**: `${XDG_CONFIG_HOME}/surfactant/config.toml`
- **Linux**: `${XDG_CONFIG_HOME}/surfactant/config.toml`

For systems that use `XDG_CONFIG_HOME`, if the environment variable is not set then the default location is `~/.config`.

## Example Configuration File

Here is an example of what the configuration file might look like:

```toml
[core]
recorded_institution = "LLNL"
```

### Initialization

To initialize the `ConfigManager`, simply import and create an instance:

```python
from surfactant.configmanager import ConfigManager

config_manager = ConfigManager()
```

This automatically handles loading a copy of the config file the first time an instance of the ConfigManager is created, effectively making it a snapshot in time of the configuration settings.

### Getting a Value

To retrieve a stored value, use the `get` method:

```python
value = config_manager.get('section', 'option', fallback='default_value')
```

- `section`: The section within the configuration file. For plugins this should be the plugin name.
- `option`: The option within the section.
- `fallback`: The fallback value if the option is not found.

Alternatively, dictionary-like access for reading is also supported:

```python
value = config_manager['section']['option']
```

However, this makes no guarantees that keys will exist and extra error handling **will be required**. If the `section` is not found then `None` is returned -- trying to access nested keys from this will fail. Furthermore, if the `section` does exist, you will need checks to see if a nested key exists before trying to access its value. A more realistic example would be:

```python
section_config = config_manager['section'] # May return `None`
value = section_config['option'] if section_config and 'option' in section_config else None
```

### Setting a Value

To set a value, use the `set` method:

```python
config_manager.set('section', 'option', 'new_value')
```

- `section`: The section within the configuration file. For plugins this should be the plugin name.
- `option`: The option within the section.
- `value`: The value to set.

NOTE: Most use cases should not need this.

### Saving the Configuration File

The configuration file is automatically saved when you set a value. The file can be manual saved using:

```python
config_manager._save_config()
```

NOTE: Most use cases should not need this.

### Loading the Configuration File

The configuration file can be reloaded using:

```python
config_manager._load_config()
```

NOTE: Most use cases should not need this.
41 changes: 37 additions & 4 deletions docs/configuration_files.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,45 @@
# Configuration Files

There are several files for configuring different aspects of Surfactant functionality based on the subcommand used.
This page currently describes the format for the file used to generate an SBOM, but will eventually cover other
configuration files as well.
This page currently describes sample configuration files, and the Surfactant settings configuration file. The sample configuration file is used to generate an SBOM for a particular software/firmware sample, and will be the most frequently written by users. The Surfactant settings configuration file is used to turn on and off various Surfactant features, including settings for controlling functionality in Surfactant plugins.

## Build configuration file
## Settings Configuration File

A configuration file contains the information about the sample to gather information from. Example JSON configuration files can be found in the examples folder of this repository.
Surfactant settings can be changed using the `surfactant config` subcommand, or by hand editing the settings configuration file (this is not the same as the JSON file used to configure settings for a particular sample that is described later).

### Command Line

Using `surfactant config` is very similar to the basic use of `git config`. The key whose value is being accessed will be in the form `section.option` where `section` is typically a plugin name or `core`, and `option` is the option to set. As an example, the `core.recorded_institution` option can be used to configure the recorded institution used to identify who the creator of a generated SBOM was.

Setting this option to `LLNL` could be done with the following command:

```bash
surfactant config core.recorded_institution LLNL
```

Getting the currently set value for the option would then be done with:

```bash
surfactant config core.recorded_institution
```

### Manual Editing

If desired, the settings config file can also be manually edited. The location of the file will depend on your platform.
On Unix-like platforms (including macOS), the XDG directory specification is followed and settings will be stored in
`${XDG_CONFIG_HOME}/surfactant/config.toml`. If the `XDG_CONFIG_HOME` environment variable is not set, the location defaults
to `~/.config`. On Windows, the file is stored in the Roaming AppData folder at `%APPDATA%\\surfactant\\config.toml`.

The file itself is a TOML file, and for the previously mentioned example plugin may look something like this:

```toml
[core]
recorded_institution = "LLNL"
```

## Build sample configuration file

A sample configuration file contains the information about the sample to gather information from. Example JSON sample configuration files can be found in the examples folder of this repository.

**extractPaths**: (required) the absolute path or relative path from location of current working directory that `surfactant` is being run from to the sample folders, cannot be a file (Note that even on Windows, Unix style `/` directory separators should be used in paths)\
**archive**: (optional) the full path, including file name, of the zip, exe installer, or other archive file that the folders in **extractPaths** were extracted from. This is used to collect metadata about the overall sample and will be added as a "Contains" relationship to all software entries found in the various **extractPaths**\
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ dependencies = [
"click==8.*",
"javatools>=1.6,==1.*",
"loguru==0.7.*",
"flask==3.*"
"flask==3.*",
"tomlkit==0.13.*",
]
dynamic = ["version"]

Expand Down
2 changes: 2 additions & 0 deletions surfactant/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from loguru import logger

from surfactant.cmd.cli import add, edit, find
from surfactant.cmd.config import config
from surfactant.cmd.createconfig import create_config
from surfactant.cmd.generate import sbom as generate
from surfactant.cmd.merge import merge_command
Expand Down Expand Up @@ -55,6 +56,7 @@ def cli():
# Main Commands
main.add_command(generate)
main.add_command(version)
main.add_command(config)
main.add_command(stat)
main.add_command(merge_command)
main.add_command(create_config)
Expand Down
51 changes: 51 additions & 0 deletions surfactant/cmd/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import List, Optional

import click

from surfactant.configmanager import ConfigManager


@click.command("config")
@click.argument("key", required=True)
@click.argument("values", nargs=-1)
def config(key: str, values: Optional[List[str]]):
"""Get or set a configuration value.
If only KEY is provided, the current value is displayed.
If both KEY and one or more VALUES are provided, the configuration value is set.
KEY should be in the format 'section.option'.
"""
config_manager = ConfigManager()

if not values:
# Get the configuration value
try:
section, option = key.split(".", 1)
except ValueError as err:
raise SystemExit("Invalid KEY given. Is it in the format 'section.option'?") from err
result = config_manager.get(section, option)
if result is None:
click.echo(f"Configuration '{key}' not found.")
else:
click.echo(f"{key} = {result}")
else:
# Set the configuration value
# Convert 'true' and 'false' strings to boolean
converted_values = []
for value in values:
if value.lower() == "true":
converted_values.append(True)
elif value.lower() == "false":
converted_values.append(False)
else:
converted_values.append(value)

# If there's only one value, store it as a single value, otherwise store as a list
final_value = converted_values[0] if len(converted_values) == 1 else converted_values

try:
section, option = key.split(".", 1)
except ValueError as err:
raise SystemExit("Invalid KEY given. Is it in the format 'section.option'?") from err
config_manager.set(section, option, final_value)
click.echo(f"Configuration '{key}' set to '{final_value}'.")
23 changes: 19 additions & 4 deletions surfactant/cmd/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import pathlib
import queue
import re
from typing import Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union

import click
from loguru import logger

from surfactant import ContextEntry
from surfactant.configmanager import ConfigManager
from surfactant.fileinfo import sha256sum
from surfactant.plugin.manager import find_io_plugin, get_plugin_manager
from surfactant.relationships import parse_relationships
Expand Down Expand Up @@ -144,6 +145,20 @@ def warn_if_hash_collision(soft1: Optional[Software], soft2: Optional[Software])
)


def get_default_from_config(option: str, fallback: Optional[Any] = None) -> Any:
"""Retrive a core config option for use as default argument value.
Args:
option (str): The core config option to get.
fallback (Optional[Any]): The fallback value if the option is not found.
Returns:
Any: The configuration value or 'NoneType' if the key doesn't exist.
"""
config_manager = ConfigManager()
return config_manager.get("core", option, fallback=fallback)


@click.command("generate")
@click.argument(
"config_file",
Expand Down Expand Up @@ -177,13 +192,13 @@ def warn_if_hash_collision(soft1: Optional[Software], soft2: Optional[Software])
@click.option(
"--recorded_institution",
is_flag=False,
default="LLNL",
default=get_default_from_config("recorded_institution"),
help="Name of user's institution",
)
@click.option(
"--output_format",
is_flag=False,
default="surfactant.output.cytrics_writer",
default=get_default_from_config("output_format", fallback="surfactant.output.cytrics_writer"),
help="SBOM output format, see --list-output-formats for list of options; default is CyTRICS",
)
@click.option(
Expand Down Expand Up @@ -221,7 +236,7 @@ def sbom(
):
"""Generate a sbom configured in CONFIG_FILE and output to SBOM_OUTPUT.
An optional INPUT_SBOM can be supplied to use as a base for subsequent operations
An optional INPUT_SBOM can be supplied to use as a base for subsequent operations.
"""

pm = get_plugin_manager()
Expand Down
Loading

0 comments on commit d98377e

Please sign in to comment.