A scoring pipeline for the AIxCC competition.
This repo uses Git submodules. Please make sure to run git submodule update --init --recursive if this is the first time you've cloned the repo.
We use dev containers for our development process. In VSCode, usage is as simple as using the dev containers extension; you may have a different experience with your chosen editor.
The scoring pipeline uses PostgreSQL for database functionality. Paired with SQLModel ( a rider on top of SQLAlchemy ), we've got a powerful toolkit. To interact with a development version of this database, ensure that you copy .devcontainer/.env.example to .devcontainer/.env, and add your desired credentials into it.
In VSCode, click the little green arrow icon in the bottom-left corner, and say "open in container". The container should build, and you should be able to drop into a shell.
If you don't wish to use dev containers, your mileage may vary. As it stands, we're using ubuntu linux 22.04 with python 3.11.6 and poetry 2.1.1.
When using Ubuntu, dependencies can be installed using the setup script in the .devcontainer folder:
./.devcontainer/setup_devcontainer.shThis script does not need to be run if using a dev container as it is called during setup.
This is a poetry project. To generate the necessary dependencies and install the package, you can run ( on linux ):
poetry install # Installs the package and dependencies
eval $(poetry env activate) # Activates the virtual environmentYou don't need to run poetry install if you're using a dev container. There will be an environment ready for you, and inside of the container you can simply run the activation command.
We use pre-commit to vet our code. You can add it with poetry run pre-commit install, and then utilize it against the python files in the project manually by running the top-level run_pre_commit_all.sh script. You can skip the pre-commit step by running git commit -m "... your message ..." --no-verify, but it isn't really recommended.
You can run the scoring pipeline in two ways:
- Follow "Dev Setup" above
- Get the correct environment variables together
- Download an audit log and place it at the top of the repository with the correct name, e.g.
audit-<round id>.jsonl - Run
poetry run python3 scoring/plugins/runner.py -s -r <round id>( e.g. final -- this is based on your audit log file name. Collaborate with other scoring team members, because caching depends on this ) from the top of the repository
And the pipeline will execute.
The scoring pipeline also provides a REST API for remote execution. The API server can be started with:
poetry run uvicorn --reload --host 0.0.0.0 --port 8000 scoring.api.main:appStart a new scoring run with an uploaded audit log file.
Parameters:
audit_log: (file, required): The audit log file to processround_id: (string, required): Round ID for the scoring rundisable_pov_validation: (boolean, optional): Disable PoV validation during scoring (default:false)force_reprocessing: (boolean, optional): Force reprocessing of cached results (default:false)skip_scantron_tests: (boolean, optional): Skip scantron tests during scoring (default:true)rescore_passed: (boolean, optional): Rescore passed results from the audit log (default:false)rescore_failed: (boolean, optional): Rescore failed results from the audit log (default:false)rescore_errored: (boolean, optional): Rescore errored results from the audit log (default:false)
Example using curl:
# Basic usage with defaults
curl -X POST "localhost:8000/v1/scoring/start" \
-F "audit_log=@audit-final.jsonl" \
-F "round_id=final"
# With custom parameters
curl -X POST "localhost:8000/v1/scoring/start" \
-F "audit_log=@audit-final.jsonl" \
-F "round_id=final" \
-F "disable_pov_validation=true" \
-F "force_reprocessing=true" \
-F "skip_scantron_tests=false" \
-F "rescore_failed=true"Response:
{
"scoring_run_uuid": "550e8400-e29b-41d4-a716-446655440000",
"message": "Scoring run 550e8400-e29b-41d4-a716-446655440000 started successfully"
}Get the current status of a scoring run.
Response:
{
"scoring_run_uuid": "550e8400-e29b-41d4-a716-446655440000",
"status": "running",
"round_id": "final",
"created_at": "2025-01-15T10:30:00Z",
"database_name": "scoring_run_550e8400_e29b_41d4_a716_446655440000"
}Get logs for a specific scoring run (placeholder implementation).
List all scoring runs (placeholder implementation).
Health check endpoint.
Response:
{
"status": "healthy",
"service": "scoring-pipeline-api",
"version": "1.0.0"
}The API exposes the same configuration options available in the CLI:
-
disable_pov_validation: Whentrue, skips validation of Proof-of-Vulnerability (PoV) execution results. Useful for faster processing when PoV validation isn't required. -
force_reprocessing: Whentrue, forces reprocessing of all cached results, ignoring previous computation results. This will significantly increase processing time but ensures fresh results. -
skip_scantron_tests: Whenfalse, includes scantron API tests in the scoring process. By default (true), these tests are skipped for faster execution. -
rescore_failed: Whentrue, attempts to rescore results that were previously marked as failed. Useful for reprocessing after fixes to scoring logic. -
rescore_passed: Whentrue, attempts to rescore results that were previously marked as passed. Useful for reprocessing after fixes to scoring logic. -
rescore_errored: Whentrue, attempts to rescore results that were previously marked as errored. Useful for reprocessing after fixes to scoring logic.
In production, the API server starts automatically as part of the Docker container's ENTRYPOINT. The server is accessible on port 8000 and joins the Tailscale network for secure access.
Various environment variables can be configured to connect to external services and modify the operation of the scoring pipeline.
Logging level configuration:
LOG_LEVEL: Defaults toINFO. Other options areDEBUG,INFO,WARNING,ERROR, andCRITICAL
Postgres database configuration variables:
POSTGRES_HOST: Address of the Postgres hostPOSTGRES_PORT: Port for the Postgres hostPOSTGRES_DB: Name of the database on the Postgres hostPOSTGRES_USER: Username for the Postgres databasePOSTGRES_PASSWORD: Password for the Postgres user
AWS storage is used to store artifacts that are related to the competition. If these need to be downloaded, we source them from S3.
AWS_STORAGE_URI: AWS server URIAWS_ACCESS_KEY: AWS access keyAWS_SECRET_KEY: AWS secret key
NOTE: By default, we assume that the artefacts will be under the prefix workingset/auditlog/final/objects. If your artefacts are somewhere else, you can set a different prefix with the environment variable STORAGE_PREFIX_OVERRIDE. DO NOT include a leading /. For instance, nightly runs' artefacts are stored elsewhere.
Maximum thread count management for parent and child tasks.
MAX_PARENT_TASK_THREADS: Maximum number of parent threads used to spawn child threads. Defaults to 1/2 the number of processors for the host operating system.MAX_CHILD_TASK_THREADS: Maximum number of child threads used to perform work. Defaults to the number of processors for the host operating system.
Deduplication Plugin Configuration. These variables control which plugins are used for PoV and Patch deduplication:
POV_DEDUP_PLUGINS: Controls which PoV deduplication plugins to usePATCH_DEDUP_PLUGINS: Controls which patch deduplication plugins to use
Both variables can be set to:
-
"DEFAULT": Use the default plugin set for that type -
Comma-separated Plugin Names: Use only the fowling available plugins in a comma separated list:
-
PoV plugins:
PoVIsCopy,PoVIsMatch,PoVPatchValueAddedDeduplicator -
Patch plugins:
PatchIsCopy,PatchIsMatch,PatchVulnDeduplicator
-
Example usage:
# Use only specific PoV plugins
POV_DEDUP_PLUGINS="PoVIsCopy,PoVIsMatch"
# Use default patch plugins
PATCH_DEDUP_PLUGINS="DEFAULT"Note: 'Scantron' is our colloquial term for the Competition API built by the AIxCC team. Those terms can ( and will ) be used interchangeably.
'Scantron' Scoring API Credentials. These help us do our patch computations. Scantron will store previously calculated results for requests that are retrieved using the cache key when available.
SCANTRON_FQDN: Fully qualified domain address of the scantron serverSCANTRON_PORT: Port of the scantron APISCANTRON_USER: Username for the scantron APISCANTRON_TOKEN: User token for the scantron APISCANTRON_SCHEME: Request type for scantron. Defaults toHTTPSSCANTRON_TIMEOUT: Scantron job timeout. Defaults to 60 secondsSCANTRON_PRERUN_TIMEOUT_HOURS: Max runtime for calculating all of the patch and PoV combination with scantron. Defaults to 48 hoursSCANTRON_CACHE_VERSION: Version key to seed the cache key for cached scantron requests. Defaults to1
Resources directory stores the input and output files for the scoring run.
RESOURCES_PATH: Path to the resources directory. Defaults to/resources
Scantron cache depends on the UUIDs of challenge tasks, patches, and PoVs. These UUIDs can be used to override or generate new scantron caches. The file stored at CACHE_REMAP_FILEPATH will remap old UUIDs to new UUIDs. CACHE_OVERRIDE_FILEPATH Points to a file that invalidates the cache entries containing any UUIDs stored in the file.
CACHE_REMAP_FILEPATH: Path to a cache JSON file used to remap UUIDs. This file should contain a mapping from old Task, Patch, and PoV UUIDs to new UUIDs. When a cache entry contains an old UUID as a key in this mapping, it will be remapped to the corresponding new UUID, and a new calculation will be performed. The file must be in the following JSON format:
{
"old_uuid_a": "new_uuid_a",
"old_uuid_b": "new_uuid_b",
...
}
CACHE_OVERRIDE_FILEPATH: Path to a JSON file used to override cached scantron results. This file contains a list of UUIDs that will invalidate a current cache entry in scantron and request a new calculation to be performed. The file must be in the JSON format:
[
"uuid_a",
"uuid_b",
...
]
Please see the README at scoring/model/README.md.
Tests are found in the tests directory. Using a containerized setup like above, you can run the current set of tests using poetry run pytest tests/db/, for example. We use a pytest fixture for our testing setup: you can see an example of getting a test database adapter and using it in a test under tests/db/test_plugin_load_audit_logs.py.
Use your favorite client. We recommend pgAdmin, because it is free and open-source.
You'll need to add the server. Press the big 'Add Server' button, and navigate to the 'Connection' tab. It should look something like this, based on the credentials in your .devcontainer/.env:
The exception is Host name/address, which needs to match the service entry for the database in .devcontainer/docker-compose.yml.
You may want to just give a copy of the database to somebody. Good for you, helping out. You can use the pg_dump command like this to do so:
pg_dump -h localhost \
-p 5432 \
-U postgres \
-d scoring_pipeline_database \
--create \
--format=c \
-f 2025-05-15-commit-4e18b50ae54-final.dumpWhere -f supplies the name of the dump once it's done. As a note, if your customer is going to use the scoring pipeline ORM for database accesses, it's important that you provide them the commit at which the data was generated so that they can check out the correct commit of the scoring pipeline. I put mine right in the filename. No, .dump is not the required file extension.
Restoring the dump is not difficult. You should be running a postgres instance locally. May I suggest:
docker run -e POSTGRES_PASSWORD=postgres -p 5433:5432 postgres:15The version of postgres that you run in order to restore the dump needs to coincide with the version of pg_dump that was used to produce the dump in the first place. Postgres is very finicky with versions, and so if I have e.g. pg_dump version 16 but you run a Postgres 14 container, you may not be able to pg_restore my dump to that container.
From a terminal you can run
pg_restore -h localhost \
-p 5433 \
-U postgres \
-d postgres \
--create /path/to/the/file/to/restore.dumpDatabase dumps can be large. The de-facto standard way I've come up with for sharing them with our team is to place them in Google Cloud Storage, in the afc-dev-share bucket, and then pointing people at them. This allows us to keep a record of the database dumps we've saved and shared.
The scoring pipeline is an extremely basic data pipeline with an underlying postgres database and a finite number of worker plugins.
The Scoring Pipeline runs very simply: it hosts a number of plugins that are run in a particular order. When one plugin ends, the runner will run the next plugin. Statuses of the runs are saved to the database. Barring a serious error, the pipeline will run to the end.
flowchart LR;
scorer_runner(Scorer Runner);
plugin_1(Plugin 1);
plugin_n(Plugin N);
postgres[(Postgres)];
scorer_runner -- Do Work --> plugin_1;
plugin_1 -- Work Results, Processing Status --> postgres;
plugin_1 -- OK, Done --> scorer_runner
scorer_runner -- Do Work --> plugin_n;
plugin_n -- OK, Done --> scorer_runner;
plugin_n -- Work Results, Processing Status --> postgres;
The scorer runner does its best to run idempotently, and also attempts to skip re-execution of tasks whenever it identifies that it has already done a block of work. The most common skipping criteria is the identification of a completed work run for a combination of Task, Team, and Plugin Type. That is to say -- if the plugin has already run for a given task for a given team, it won't run again unless force_reprocess is found in the input event. This determination is made based on the process status table.
Workloads: Using the Competition API
Some plugins need to evaluate vulnerabilities and patches, and therefore need our infrastructure for build_cr, etc. These tasks will outsource these efforts to the Competition API, which has a number of capabilities:
- Running a PoV in a certain challenge repository
- Patching a particular PoV and verifying that the patch functions correctly ( or not! )
- Verifying that a certain challenge repository works correctly
Patch computation is very easily the most difficult and time consuming part of the pipeline. In order to determine whether a patch mitigates a given vulnerability, we must determine that the patch mitigates every PoV bucketed to that vulnerability, and thus we must at least test patches against PoVs until one fails. We do have some deduplication measures in-place, but no matter what, this is going to be a large amount of compute. Without concurrency and a very large amount of compute power, it would take far too long to compute everything that we need to.
Our system does not make these computations. Instead, we use the Competition API to make the computations happen. You can see the calls for this in the Patch Evaluation plugin in patch_evaluation.py.
Each individual patch evaluation call isn't necessarily fast. It can take on the order of tens of minutes to confirm that a given patch mitigates a given PoV. It's necessary, therefore, that we scale this as much as we possibly can. So, the scoring pipeline threads, carefully ensuring that we do not overload our host machine. We will send all of the requests as quickly as possible, and then iterate over them to assess their statuses according to scantron.
In order to set the maximum number of threads that your machine will use to execute, you can use the MAX_THREADS environment variable. See scoring/environment.py.
You can run just the work method of a given plugin if you want to test it alone with data from the database using the included script at scoring/scripts/run_plugin.py. Its typical usage it like
.../scoring/scripts $ poetry run python3 run_plugin.py \
-c <challenge task UUID>
-t <team UUID>
-p <plugin name>- The database model can be found in
scoring/model/__init__.py - The task modules can be found under
scoring/plugins/tasks - Once you've got a shell in the dev container...
- Activate the virtual environment with
eval $(poetry env activate) - You can run the Scorer Runner with
python3 scoring/plugins/runner.py - Access the database at
localhost:5432with your favorite database tools - If you want to destroy all of the data, execute the commands from
scoring/scripts/trash_all_data.sql
- Activate the virtual environment with
