Skip to content

Commit 92b942a

Browse files
committed
automate openvex creation via GH Securities
- automate the creation a pull request that contains OpenVEX statements from known GH Securities. To do this, the script pushes to upstream a new branch, named `vex` and creates the pull request against `master`. the branch `vex` is always created on top of `master` and contains new OpenVEX statements for the last three releases of Erlang/OTP. If there is already an open pull request for `vex`, the script skips pushing more stuff. when the pull request is merged, a new pull request will be created. the pull request creation is scheduled on a daily basis. - this PR also updated and formatted the openvex.table with missing CVEs and wrongly reported initial versions.
1 parent 96bef0d commit 92b942a

File tree

8 files changed

+536
-307
lines changed

8 files changed

+536
-307
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env sh
2+
3+
## %CopyrightBegin%
4+
##
5+
## SPDX-License-Identifier: Apache-2.0
6+
##
7+
## Copyright Ericsson AB 2026. All Rights Reserved.
8+
##
9+
## Licensed under the Apache License, Version 2.0 (the "License");
10+
## you may not use this file except in compliance with the License.
11+
## You may obtain a copy of the License at
12+
##
13+
## http://www.apache.org/licenses/LICENSE-2.0
14+
##
15+
## Unless required by applicable law or agreed to in writing, software
16+
## distributed under the License is distributed on an "AS IS" BASIS,
17+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
## See the License for the specific language governing permissions and
19+
## limitations under the License.
20+
##
21+
## %CopyrightEnd%
22+
23+
24+
REPO=$1
25+
BRANCH_NAME=$2
26+
# Fetch PR data using gh CLI
27+
PR_STATUS=$(gh pr view "$BRANCH_NAME" --repo "$REPO" --json state -q ".state")
28+
29+
if [ $? -ne 0 ]; then
30+
echo "Failed to fetch PR #$BRANCH_NAME from $REPO"
31+
exit 2
32+
fi
33+
34+
git config user.name "github-actions[bot]"
35+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
36+
37+
# Check if PR is closed
38+
if [ "$PR_STATUS" = "CLOSED" ] || [ "$PR_STATUS" = "MERGED" ]; then
39+
echo "✅ Pull request #$BRANCH_NAME is CLOSED or MERGED."
40+
git branch "$BRANCH_NAME" master
41+
git checkout "$BRANCH_NAME"
42+
git add make/openvex.table
43+
git add vex
44+
git commit -m "Automatic update of OpenVEX Statements for erlang/otp"
45+
git push --force origin "$BRANCH_NAME"
46+
gh pr create --repo "$REPO" -B master \
47+
--title "Automatic update of OpenVEX Statements for erlang/otp" \
48+
--body "Automatic Action. There is a vulnerability from GH Advisories without a matching OpenVEX statement"
49+
exit 0
50+
else
51+
echo "❌ Pull request #$BRANCH_NAME is OPEN. Create a PR once the PR is closed or merged."
52+
exit 0
53+
fi

.github/scripts/otp-compliance.es

Lines changed: 126 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,23 @@
9696
%% VEX MACROS
9797
%%
9898
-define(VexPath, ~"vex/").
99+
-define(OpenVEXTablePath, "make/openvex.table").
99100
-define(ErlangPURL, "pkg:github/erlang/otp").
100101

101102
-define(FOUND_VENDOR_VULNERABILITY_TITLE, "Vendor vulnerability found").
102103
-define(FOUND_VENDOR_VULNERABILITY, lists:append(string:replace(?FOUND_VENDOR_VULNERABILITY_TITLE, " ", "+", all))).
103104

105+
-define(OTP_GH_URI, "https://raw.githubusercontent.com/" ++ ?GH_ACCOUNT ++ "/refs/heads/master/").
106+
104107
%% GH default options
105108
-define(GH_ADVISORIES_OPTIONS, "state=published&direction=desc&per_page=100&sort=updated").
106109

107110
%% Advisories to download from last X years.
108111
-define(GH_ADVISORIES_FROM_LAST_X_YEARS, 5).
109112

113+
%% Defines path of script to create PRs for missing openvex/vulnerabilities
114+
-define(CREATE_OPENVEX_PR_SCRIPT_FILE, ".github/scripts/create-openvex-pr.sh").
115+
110116
%% Sets end point account to fetch information from GH
111117
%% used by `gh` command-line tool.
112118
%% change to your fork for testing, e.g., `kikofernandez/otp`
@@ -260,7 +266,8 @@ cli() ->
260266
"osv-scan" =>
261267
#{ help =>
262268
"""
263-
Performs vulnerability scanning on vendor libraries
269+
Performs vulnerability scanning on vendor libraries.
270+
As a side effect,
264271
265272
Example:
266273
@@ -295,10 +302,15 @@ cli() ->
295302
#{ help =>
296303
"""
297304
Download Github Advisories for erlang/otp.
298-
Checks that those are present in OpenVEX statements.
305+
Download OpenVEX statement from erlang/otp for the selected branch.
306+
Checks that those Advisories are present in OpenVEX statements.
299307
Creates PR for any non-present Github Advisory.
308+
309+
Example:
310+
> .github/scripts/otp-compliance.es vex verify -p
311+
300312
""",
301-
arguments => [branch_option(), vex_path_option()],
313+
arguments => [create_pr()],
302314
handler => fun verify_openvex/1
303315
},
304316

@@ -480,6 +492,13 @@ vex_path_option() ->
480492
help => "Path to folder containing openvex statements, e.g., `vex/`",
481493
long => "-vex-path"}.
482494

495+
create_pr() ->
496+
#{name => create_pr,
497+
short => $p,
498+
type => boolean,
499+
default => false,
500+
help => "Indicates if missing OpenVEX statements create and submit a PR"}.
501+
483502
%%
484503
%% Commands
485504
%%
@@ -1490,7 +1509,7 @@ create_gh_issue(Version, Title, BodyText) ->
14901509
ok.
14911510

14921511
ignore_vex_cves(Branch, Vulns) ->
1493-
OpenVex = get_otp_openvex_file(Branch),
1512+
OpenVex = download_otp_openvex_file(Branch),
14941513
OpenVex1 = format_vex_statements(OpenVex),
14951514

14961515
case OpenVex1 of
@@ -1537,33 +1556,54 @@ format_vex_statements(OpenVex) ->
15371556
Result ++ Acc
15381557
end, [], Stmts).
15391558

1540-
get_otp_openvex_file(Branch) ->
1541-
OpenVexPath = fetch_openvex_filename(Branch),
1559+
read_openvex_file(Branch) ->
1560+
_ = create_dir(?VexPath),
1561+
OpenVexPath = path_to_openvex_filename(Branch),
1562+
OpenVexStr = erlang:binary_to_list(OpenVexPath),
1563+
decode(OpenVexStr).
1564+
1565+
-spec download_otp_openvex_file(Branch :: binary()) -> Json :: map() | EmptyMap :: #{} | no_return().
1566+
download_otp_openvex_file(Branch) ->
1567+
_ = create_dir(?VexPath),
1568+
OpenVexPath = path_to_openvex_filename(Branch),
15421569
OpenVexStr = erlang:binary_to_list(OpenVexPath),
1543-
GithubURI = "https://raw.githubusercontent.com/" ++ ?GH_ACCOUNT ++ "/refs/heads/master/" ++ OpenVexStr,
1570+
GithubURI = get_gh_download_uri(OpenVexStr),
15441571

15451572
io:format("Checking OpenVex statements in '~s' from~n'~s'...~n", [OpenVexPath, GithubURI]),
15461573

15471574
ValidURI = "curl -I -Lj --silent " ++ GithubURI ++ " | head -n1 | cut -d' ' -f2",
15481575
case string:trim(os:cmd(ValidURI)) of
15491576
"200" ->
1577+
%% Overrides existing file.
15501578
io:format("OpenVex file found.~n~n"),
15511579
Command = "curl -LJ " ++ GithubURI ++ " --output " ++ OpenVexStr,
1580+
io:format("Proceed to download:~n~s~n~n", [Command]),
15521581
os:cmd(Command, #{ exception_on_failure => true }),
15531582
decode(OpenVexStr);
15541583
E ->
1555-
io:format("[~p] No OpenVex file found.~n~n", [E]),
1584+
io:format("[~p] No OpenVex statements found for file '~s'.~n~n", [E, OpenVexStr]),
15561585
#{}
15571586
end.
15581587

1559-
fetch_openvex_filename(Branch) ->
1588+
-spec get_gh_download_uri(String :: list()) -> String :: list().
1589+
get_gh_download_uri(File) ->
1590+
?OTP_GH_URI ++ File.
1591+
1592+
-spec create_dir(DirName :: binary()) -> ok | no_return().
1593+
create_dir(DirName) ->
1594+
case file:make_dir(DirName) of
1595+
Result when Result == ok;
1596+
Result == {error, eexist} ->
1597+
io:format("Directory ~s created successfully.~n", [DirName]);
1598+
{error, Reason} ->
1599+
fail("Failed to create directory ~s: ~p~n", [DirName, Reason])
1600+
end.
1601+
1602+
-spec path_to_openvex_filename(Branch :: binary()) -> Path :: binary().
1603+
path_to_openvex_filename(Branch) ->
15601604
_ = valid_scan_branches(Branch),
15611605
Version = maint_to_otp_conversion(Branch),
15621606
vex_path(Version).
1563-
fetch_openvex_filename(Branch, VexPath) ->
1564-
_ = valid_scan_branches(Branch),
1565-
Version = maint_to_otp_conversion(Branch),
1566-
vex_path(VexPath, Version).
15671607

15681608
maint_to_otp_conversion(Branch) ->
15691609
case Branch of
@@ -1581,6 +1621,7 @@ maint_to_otp_conversion(Branch) ->
15811621
OTP
15821622
end.
15831623

1624+
-spec valid_scan_branches(Branch :: binary()) -> ok | no_return().
15841625
valid_scan_branches(Branch) ->
15851626
case Branch of
15861627
~"master" ->
@@ -2467,28 +2508,80 @@ run_openvex1(VexStmts, VexTableFile, Branch, VexPath) ->
24672508
Statements = calculate_statements(VexStmts, VexTableFile, Branch, VexPath),
24682509
lists:foreach(fun (St) -> io:format("~ts", [St]) end, Statements).
24692510

2470-
verify_openvex(#{branch := Branch, vex_path := VexPath}) ->
2471-
UpdatedBranch = maint_to_otp_conversion(Branch),
2472-
OpenVEX = read_openvex(VexPath, UpdatedBranch),
2473-
Advisory = download_advisory_from_branch(UpdatedBranch),
2474-
case verify_advisory_against_openvex(OpenVEX, Advisory) of
2475-
[] ->
2476-
ok;
2477-
MissingAdvisories when is_list(MissingAdvisories) ->
2478-
create_advisory(MissingAdvisories)
2479-
end.
2480-
2481-
read_openvex(VexPath, Branch) ->
2482-
InitVex = fetch_openvex_filename(Branch, VexPath),
2483-
case filelib:is_file(InitVex) of
2484-
true -> % file exists
2485-
decode(InitVex);
2511+
verify_openvex(#{create_pr := PR}) ->
2512+
Branches = get_supported_branches(),
2513+
io:format("Sync ~p~n", [Branches]),
2514+
_ = lists:foreach(
2515+
fun (Branch) ->
2516+
case verify_openvex_advisories(Branch) of
2517+
[] ->
2518+
io:format("No new advisories nor OpenVEX statements created for '~s'.", [Branch]);
2519+
MissingAdvisories ->
2520+
io:format("Missing Advisories:~n~p~n~n", [MissingAdvisories]),
2521+
case PR of
2522+
false ->
2523+
io:format("To automatically update openvex.table and create a PR run:~n" ++
2524+
".github/scripts/otp-compliance.es vex verify -b ~s -p~n~n", [Branch]);
2525+
true ->
2526+
Advs = create_advisory(MissingAdvisories),
2527+
_ = update_openvex_otp_table(Branch, Advs),
2528+
BranchStr = erlang:binary_to_list(Branch),
2529+
_ = cmd(".github/scripts/otp-compliance.es vex run -b "++ BranchStr ++ " | bash")
2530+
end
2531+
end
2532+
end, Branches),
2533+
case PR of
2534+
true ->
2535+
cmd(".github/scripts/create-openvex-pr.sh " ++ ?GH_ACCOUNT ++ " vex");
24862536
false ->
2487-
throw(file_not_found)
2537+
ok
24882538
end.
24892539

2540+
verify_openvex_advisories(Branch) ->
2541+
OpenVEX = read_openvex_file(Branch),
2542+
Advisory = download_advisory_from_branch(Branch),
2543+
verify_advisory_against_openvex(OpenVEX, Advisory).
2544+
2545+
-spec get_supported_branches() -> [Branches :: binary()].
2546+
get_supported_branches() ->
2547+
Branches = cmd(".github/scripts/get-supported-branches.sh"),
2548+
BranchesBin = json:decode(erlang:list_to_binary(Branches)),
2549+
io:format("~p~n~p~n", [Branches, BranchesBin]),
2550+
lists:filtermap(fun (<<"maint-", _/binary>>=OTP) -> {true, maint_to_otp_conversion(OTP)};
2551+
(_) -> false
2552+
end, BranchesBin).
2553+
24902554
create_advisory(Advisories) ->
2491-
io:format("Missing:~n~p~n~n", [Advisories]).
2555+
lists:foldl(fun (Adv, Acc) ->
2556+
create_openvex_otp_entries(Adv) ++ Acc
2557+
end, [], Advisories).
2558+
2559+
create_openvex_otp_entries(#{'CVE' := CVEId,
2560+
'appName' := AppName,
2561+
'affectedVersions' := AffectedVersions,
2562+
'fixedVersions' := FixedVersions}) ->
2563+
AppFixedVersions = lists:map(fun (Ver) -> create_app_purl(AppName, Ver) end, FixedVersions),
2564+
lists:map(fun (Affected) ->
2565+
Purl = create_app_purl(AppName, Affected),
2566+
create_openvex_app_entry(Purl, CVEId, AppFixedVersions)
2567+
end, AffectedVersions).
2568+
2569+
create_app_purl(AppName, Version) when is_binary(AppName), is_binary(Version) ->
2570+
<<"pkg:otp/", AppName/binary, "@", Version/binary>>.
2571+
2572+
create_openvex_app_entry(Purl, CVEId, FixedVersions) ->
2573+
#{Purl => CVEId,
2574+
~"status" =>
2575+
#{ ~"affected" => iolist_to_binary(io_lib:format("Update to any of the following versions: ~s", [FixedVersions])),
2576+
~"fixed" => FixedVersions}}.
2577+
2578+
update_openvex_otp_table(Branch, Advs) ->
2579+
Path = ?OpenVEXTablePath,
2580+
io:format("OpenVEX Statements:~n~p~n~n", [Advs]),
2581+
#{Branch := Statements}=Table = decode(Path),
2582+
UpdatedTable = Table#{Branch := Advs ++ Statements},
2583+
io:format("Update table:~n~p~n", [UpdatedTable]),
2584+
file:write_file(Path, json:format(UpdatedTable)).
24922585

24932586
generate_gh_link(Part) ->
24942587
"\"/repos/erlang/otp/security-advisories?" ++ Part ++ "\"".
@@ -2879,7 +2972,8 @@ format_vexctl(VexPath, Versions, CVE, S) when S =:= ~"fixed";
28792972
[VexPath, Versions, CVE, S]).
28802973

28812974

2882-
-spec fetch_otp_purl_versions(OTP :: binary(), FixedVersions :: [binary()] ) -> OTPAppVersions :: binary().
2975+
-spec fetch_otp_purl_versions(OTP :: binary(), FixedVersions :: [binary()] ) ->
2976+
{AffectedPurls :: binary(), FixedPurls :: binary()} | false.
28832977
fetch_otp_purl_versions(<<?ErlangPURL, _/binary>>, _FixedVersions) ->
28842978
%% ignore
28852979
false;

.github/workflows/openvex-sync.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
## %CopyrightBegin%
2+
##
3+
## SPDX-License-Identifier: Apache-2.0
4+
##
5+
## Copyright Ericsson AB 2024-2025. All Rights Reserved.
6+
##
7+
## Licensed under the Apache License, Version 2.0 (the "License");
8+
## you may not use this file except in compliance with the License.
9+
## You may obtain a copy of the License at
10+
##
11+
## http://www.apache.org/licenses/LICENSE-2.0
12+
##
13+
## Unless required by applicable law or agreed to in writing, software
14+
## distributed under the License is distributed on an "AS IS" BASIS,
15+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
## See the License for the specific language governing permissions and
17+
## limitations under the License.
18+
##
19+
## %CopyrightEnd%
20+
21+
## Periodically syncs OpenVEX files against Erlang OTP Securities,
22+
## creating an automatic PR with the missing published securities.
23+
name: OpenVEX Securities Syncing
24+
25+
on:
26+
pull_request:
27+
workflow_dispatch:
28+
schedule:
29+
- cron: 0 1 * * *
30+
31+
permissions:
32+
contents: read
33+
34+
jobs:
35+
run-scheduled-openvex-sync:
36+
runs-on: ubuntu-latest
37+
permissions:
38+
security-events: read
39+
actions: write
40+
contents: write
41+
pull-requests: write
42+
steps:
43+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/[email protected]
44+
with:
45+
ref: 'master' # '' = default branch
46+
47+
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # racket:actions/checkout@v1
48+
with:
49+
otp-version: '28'
50+
51+
- uses: openvex/setup-vexctl@e85ca48f3c8a376289f6476129d59cda82147e71 # ratchet:openvex/[email protected]
52+
with:
53+
vexctl-release: '0.3.0'
54+
55+
- name: 'Open OpenVEX Pull Requests for newly released vulnerabilities'
56+
env:
57+
GH_TOKEN: ${{ github.token }}
58+
REPO: ${{ github.repository }}
59+
run: |
60+
.github/scripts/otp-compliance.es vex verify -p

.github/workflows/osv-scanner-scheduled.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,11 @@ jobs:
5757
type: ${{ fromJson(needs.schedule-scan.outputs.versions) }}
5858
fail-fast: false
5959
permissions:
60-
actions: write
60+
security-events: read
6161
issues: write
62+
actions: write
63+
contents: write
64+
pull-requests: write
6265
steps:
6366
# this call to a workflow_dispatch ref=master is important because
6467
# using ref={{matrix.type}} would trigger the workflow

.github/workflows/reusable-vendor-vulnerability-scanner.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,6 @@ jobs:
115115
chmod +x otp-compliance.es
116116
cp otp-compliance.es /home/runner/work/otp/otp/.github/scripts/otp-compliance.es
117117
cd /home/runner/work/otp/otp && \
118-
mkdir -p vex && \
119118
.github/scripts/otp-compliance.es sbom osv-scan \
120119
--version ${{ inputs.version }} \
121120
--fail_if_cve ${{ inputs.fail_if_cve }}
122-
.github/scripts/otp-compliance.es vex verify -b ${{ inputs.version }}

0 commit comments

Comments
 (0)