Skip to content

Commit b2ce706

Browse files
committed
otp scan PRs for vulnerabilities
- perform vulnerability analysis on a pull requests basis and on a scheduled basis. - adds the option `osv-scan` to `otp-compliance.es` to submit requests to OSV API. - adds hard-coded VEX filter to ignore CVEs for which OTP is not vulnerable, using the last three releases that we maintain. - creation of reusable Github workflow to allow direct calls (workflow_call) and gh triggering events (workflow_dispatch).
1 parent 36f8d55 commit b2ce706

File tree

13 files changed

+353
-31
lines changed

13 files changed

+353
-31
lines changed

.github/scripts/otp-compliance.es

Lines changed: 229 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,19 @@ cli() ->
217217
> .github/scripts/otp-compliance.es sbom vendor --sbom-file otp.spdx.json
218218
""",
219219
arguments => [ sbom_option()],
220-
handler => fun sbom_vendor/1}
220+
handler => fun sbom_vendor/1},
221+
222+
"osv-scan" =>
223+
#{ help =>
224+
"""
225+
Performs vulnerability scanning on vendor libraries
226+
227+
Example:
228+
229+
> .github/scripts/otp-compliance.es sbom osv-scan
230+
""",
231+
arguments => [ versions_file()],
232+
handler => fun osv_scan/1}
221233
}},
222234
"explore" =>
223235
#{ help => """
@@ -317,6 +329,11 @@ sbom_option() ->
317329
default => "bom.spdx.json",
318330
long => "-sbom-file"}.
319331

332+
versions_file() ->
333+
#{name => version,
334+
type => binary,
335+
long => "-version"}.
336+
320337
ntia_checker() ->
321338
#{name => ntia_checker,
322339
type => boolean,
@@ -1207,6 +1224,216 @@ generate_vendor_purl(Package) ->
12071224
[create_externalRef_purl(Description, <<Purl/binary, "@", Vsn/binary>>)]
12081225
end.
12091226

1227+
osv_scan(#{version := Version}) ->
1228+
application:ensure_all_started([ssl, inets]),
1229+
OSVQuery = vendor_by_version(Version),
1230+
1231+
io:format("[OSV] Information sent~n~s~n", [json:format(OSVQuery)]),
1232+
1233+
OSV = json:encode(OSVQuery),
1234+
1235+
Format = "application/x-www-form-urlencoded",
1236+
URI = "https://api.osv.dev/v1/querybatch",
1237+
Content = {URI, [], Format, OSV},
1238+
Result = httpc:request(post, Content, [], []),
1239+
Vulns =
1240+
case Result of
1241+
{ok,{{_, 200,_}, _Headers, Body}} ->
1242+
#{~"results" := OSVResults} = json:decode(erlang:list_to_binary(Body)),
1243+
Vulnerabilities = lists:filter(fun (#{~"vulns" := _Ids}) -> true; (_) -> false end, OSVResults),
1244+
case Vulnerabilities of
1245+
[] ->
1246+
[];
1247+
_ ->
1248+
NameVulnerabilities = lists:zip(osv_names(OSVQuery), OSVResults),
1249+
lists:filtermap(fun ({Name, #{~"vulns" := Ids}}) ->
1250+
{true, {Name, [Id || #{~"id" := Id} <- Ids]}};
1251+
(_) ->
1252+
false
1253+
end, NameVulnerabilities)
1254+
end;
1255+
{error, Error} ->
1256+
{error, [URI, Error]}
1257+
end,
1258+
Vulns1 = ignore_vex_cves(Vulns),
1259+
FormattedVulns = format_vulnerabilities(Vulns1),
1260+
report_vulnerabilities(FormattedVulns).
1261+
1262+
%% TODO: fix by reading VEX files from erlang/vex or repo containing VEX files
1263+
ignore_vex_cves(Vulns) ->
1264+
lists:foldl(fun ({~"github.com/wxWidgets/wxWidgets", _CVEs}, Acc) ->
1265+
%% OTP cannot be vulnerable to wxwidgets because
1266+
%% we only take documentation.
1267+
Acc;
1268+
({Name, CVEs}, Acc) ->
1269+
case maps:get(Name, non_vulnerable_cves(), not_found) of
1270+
not_found ->
1271+
[{Name, CVEs} | Acc];
1272+
NonCVEs ->
1273+
case CVEs -- NonCVEs of
1274+
[] ->
1275+
Acc;
1276+
Vs ->
1277+
[{Name, Vs} | Acc]
1278+
end
1279+
end
1280+
end, [], Vulns).
1281+
1282+
non_vulnerable_cves() ->
1283+
#{ ~"github.com/madler/zlib" => [~"CVE-2023-45853"],
1284+
~"github.com/openssl/openssl" =>
1285+
[~"CVE-2024-12797", ~"CVE-2023-6129", ~"CVE-2023-6237", ~"CVE-2024-0727",
1286+
~"CVE-2024-13176", ~"CVE-2024-2511", ~"CVE-2024-4603", ~"CVE-2024-4741",
1287+
~"CVE-2024-5535", ~"CVE-2024-6119", ~"CVE-2024-9143"],
1288+
~"github.com/PCRE2Project/pcre2" => [~"OSV-2025-300"]}.
1289+
1290+
1291+
format_vulnerabilities({error, ErrorContext}) ->
1292+
{error, ErrorContext};
1293+
format_vulnerabilities(ExistingVulnerabilities) when is_list(ExistingVulnerabilities) ->
1294+
lists:map(fun ({N, Ids}) ->
1295+
io_lib:format("- ~s: ~s~n", [N, lists:join(",", Ids)])
1296+
end, ExistingVulnerabilities).
1297+
1298+
report_vulnerabilities([]) ->
1299+
io:format("[OSV] No vulnerabilities found.~n");
1300+
report_vulnerabilities({error, [URI, Error]}) ->
1301+
fail("[OSV] POST request to ~p errors: ~p", [URI, Error]);
1302+
report_vulnerabilities(FormatVulns) ->
1303+
fail("[OSV] There are existing vulnerabilities:~n~s", [FormatVulns]).
1304+
1305+
osv_names(#{~"queries" := Packages}) ->
1306+
lists:map(fun osv_names/1, Packages);
1307+
osv_names(#{~"package" := #{~"name" := Name }}) ->
1308+
Name.
1309+
1310+
generate_osv_query(Packages) ->
1311+
#{~"queries" => lists:foldl(fun generate_osv_query/2, [], Packages)}.
1312+
generate_osv_query(#{~"versionInfo" := Vsn, ~"ecosystem" := Ecosystem, ~"name" := Name}, Acc) ->
1313+
Package = #{~"package" => #{~"name" => Name, ~"ecosystem" => Ecosystem}, ~"version" => Vsn},
1314+
[Package | Acc];
1315+
generate_osv_query(#{~"sha" := SHA, ~"downloadLocation" := Location}, Acc) ->
1316+
case string:prefix(Location, ~"https://") of
1317+
nomatch ->
1318+
Acc;
1319+
URI ->
1320+
Package = #{~"package" => #{~"name" => URI}, ~"commit" => SHA},
1321+
[Package | Acc]
1322+
end;
1323+
generate_osv_query(_, Acc) ->
1324+
Acc.
1325+
1326+
vendor_by_version(~"maint-25") ->
1327+
[#{~"package" =>
1328+
#{~"commit"=> ~"21767c654d31d2dccdde4330529775c6c5fd5389",
1329+
~"name"=> ~"github.com/madler/zlib"}},
1330+
#{~"package" =>
1331+
#{~"commit"=> ~"23ddf56b00f47d8aa0c82ad225e4b3a92661da7e",
1332+
~"name"=> ~"github.com/asmjit/asmjit"}},
1333+
#{~"package" =>
1334+
#{ ~"commit"=> ~"e745bad3b1d05b5b19ec652d68abb37865ffa454",
1335+
~"name"=> ~"github.com/microsoft/STL"}},
1336+
#{~"package" =>
1337+
#{~"commit"=> ~"844864ac213bdbf1fb57e6f51c653b3d90af0937",
1338+
~"name"=> ~"github.com/ulfjack/ryu"}},
1339+
#{~"package"=>
1340+
#{ ~"commit"=> ~"01d5e2318405362b4de5e670c90d9b40a351d053",
1341+
~"name"=> ~"github.com/openssl/openssl"
1342+
}},
1343+
#{~"package"=> % 8.45, not offial but the official sourceforge is not available
1344+
#{~"commit"=> ~"3934406b50b8c2a4e2fc7362ed8026224ac90828",
1345+
~"name"=> ~"github.com/nektro/pcre-8.45"}},
1346+
#{~"package"=> % 3.1.4
1347+
#{ ~"commit"=> ~"01d5e2318405362b4de5e670c90d9b40a351d053",
1348+
~"name"=> ~"github.com/openssl/openssl"}},
1349+
#{~"package"=>
1350+
#{~"ecosystem"=> ~"npm",
1351+
~"name"=> ~"tablesorter",
1352+
~"version"=> ~"2.32"}},
1353+
#{~"package"=>
1354+
#{~"ecosystem"=> ~"npm",
1355+
~"name"=> ~"jquery",
1356+
~"version"=> ~"3.7.1"}},
1357+
#{~"package"=> % ok
1358+
#{~"commit"=> ~"dc585039bbd426829e3433002023a93f9bedd0c2",
1359+
~"name"=> ~"github.com/wxWidgets/wxWidgets"}}
1360+
];
1361+
vendor_by_version(~"maint-26") ->
1362+
[#{~"package"=> %% v1.2.13
1363+
#{~"commit"=> ~"04f42ceca40f73e2978b50e93806c2a18c1281fc",
1364+
~"name"=> ~"github.com/madler/zlib"
1365+
}},
1366+
#{~"package"=>
1367+
#{~"commit"=> ~"915186f6c5c2f5a4638e5cb97ccc23d741521a64",
1368+
~"name"=> ~"github.com/asmjit/asmjit"
1369+
}},
1370+
#{~"package"=>
1371+
#{~"commit"=> ~"e745bad3b1d05b5b19ec652d68abb37865ffa454",
1372+
~"name"=> ~"github.com/microsoft/STL"}},
1373+
#{~"package"=>
1374+
#{~"commit"=> ~"844864ac213bdbf1fb57e6f51c653b3d90af0937",
1375+
~"name"=> ~"github.com/ulfjack/ryu"}},
1376+
#{~"package"=> % 3.1.4
1377+
#{~"commit"=> ~"01d5e2318405362b4de5e670c90d9b40a351d053",
1378+
~"name"=> ~"github.com/openssl/openssl"}},
1379+
#{~"package"=> % 8.45, not offial but the official sourceforge is not available
1380+
#{~"commit"=> ~"3934406b50b8c2a4e2fc7362ed8026224ac90828",
1381+
~"name"=> ~"github.com/nektro/pcre-8.45"}},
1382+
#{~"package"=> % 3.1.4
1383+
#{~"commit"=> ~"01d5e2318405362b4de5e670c90d9b40a351d053",
1384+
~"name"=> ~"github.com/openssl/openssl"}},
1385+
#{~"package"=>
1386+
#{~"ecosystem"=> ~"npm",
1387+
~"name"=> ~"tablesorter",
1388+
~"version"=> ~"2.32"}},
1389+
#{~"package"=>
1390+
#{~"ecosystem"=> ~"npm",
1391+
~"name"=> ~"jquery",
1392+
~"version"=> ~"3.7.1"}},
1393+
#{~"package"=>
1394+
#{~"commit"=> ~"dc585039bbd426829e3433002023a93f9bedd0c2",
1395+
~"name"=> ~"github.com/wxWidgets/wxWidgets"}}
1396+
];
1397+
vendor_by_version(~"maint-27") ->
1398+
[#{~"package"=> %% v1.2.13
1399+
#{~"commit"=> ~"04f42ceca40f73e2978b50e93806c2a18c1281fc",
1400+
~"name"=> ~"github.com/madler/zlib"}},
1401+
#{~"package"=>
1402+
#{~"commit"=> ~"a465fe71ab3d0e224b2b4bd0fac69ae68ab9239d",
1403+
~"name"=> ~"github.com/asmjit/asmjit"
1404+
}},
1405+
#{~"package"=>
1406+
#{~"commit"=> ~"e745bad3b1d05b5b19ec652d68abb37865ffa454",
1407+
~"name"=> ~"github.com/microsoft/STL"}},
1408+
#{~"package"=>
1409+
#{~"commit"=> ~"844864ac213bdbf1fb57e6f51c653b3d90af0937",
1410+
~"name"=> ~"github.com/ulfjack/ryu"}},
1411+
#{~"package"=> % 3.1.4
1412+
#{~"commit"=> ~"01d5e2318405362b4de5e670c90d9b40a351d053",
1413+
~"name"=> ~"github.com/openssl/openssl"}},
1414+
#{~"package"=> % 8.45, not offial but the official sourceforge is not available
1415+
#{~"commit"=> ~"3934406b50b8c2a4e2fc7362ed8026224ac90828",
1416+
~"name"=> ~"github.com/nektro/pcre-8.45"}},
1417+
#{~"package"=> % 3.1.4
1418+
#{~"commit"=> ~"01d5e2318405362b4de5e670c90d9b40a351d053",
1419+
~"name"=> ~"github.com/openssl/openssl"}},
1420+
#{~"package"=>
1421+
#{~"ecosystem"=> ~"npm",
1422+
~"name"=> ~"tablesorter",
1423+
~"version"=> ~"2.32"}},
1424+
#{~"package"=>
1425+
#{~"ecosystem"=> ~"npm",
1426+
~"name"=> ~"jquery",
1427+
~"version"=> ~"3.7.1"}},
1428+
#{~"package"=>
1429+
#{~"commit"=> ~"dc585039bbd426829e3433002023a93f9bedd0c2",
1430+
~"name"=> ~"github.com/wxWidgets/wxWidgets"}}
1431+
];
1432+
vendor_by_version(_) ->
1433+
VendorSrcFiles = find_vendor_src_files("."),
1434+
Packages = generate_vendor_info_package(VendorSrcFiles),
1435+
generate_osv_query(Packages).
1436+
12101437
cleanup_path(<<"./", Path/binary>>) when is_binary(Path) -> Path;
12111438
cleanup_path(Path) when is_binary(Path) -> Path.
12121439

@@ -1559,7 +1786,7 @@ root_vendor_packages() ->
15591786
minimum_vendor_packages() ->
15601787
%% self-contained
15611788
root_vendor_packages() ++
1562-
[~"tcl", ~"ryu_to_chars", ~"json-test-suite", ~"openssl", ~"Autoconf", ~"wx", ~"jquery", ~"jquery-tablesorter"].
1789+
[~"tcl", ~"ryu_to_chars", ~"json-test-suite", ~"openssl", ~"Autoconf", ~"wx", ~"jquery", ~"tablesorter"].
15631790

15641791
test_copyright_not_empty(#{~"packages" := Packages}) ->
15651792
true = lists:all(fun (#{~"copyrightText" := Copyright}) -> Copyright =/= ~"" end, Packages),

.github/workflows/main.yaml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,15 @@ jobs:
426426
docker run otp "erl ${OPTION} -noshell -s init stop"
427427
done
428428
429+
# this is a call to a workflow_call
430+
pr-vendor-vulnerability-analysis:
431+
name: Vendor Vulnerability Scanning
432+
needs: pack
433+
uses: ./.github/workflows/reusable-vendor-vulnerability-scanner.yml
434+
with:
435+
version: ${{ github.event_name == 'pull_request' && github.base_ref || github.ref_name }}
436+
# equivalent of ${{ env.BASE_BRANCH }} but reusable-workflows do not allow to pass env.
437+
429438
build:
430439
name: Build Erlang/OTP
431440
runs-on: ubuntu-latest
@@ -807,6 +816,10 @@ jobs:
807816
path: ${{ env.SCAN_RESULT_CACHE_PATH }}
808817
key: ${{ steps.ort-cache.outputs.cache-primary-key }}
809818

819+
- name: Copy PreSBOM
820+
run: |
821+
cp /home/runner/.ort/ort-results/bom.spdx.json /home/runner/.ort/ort-results/pre-bom.spdx.json
822+
810823
- name: Process SBOM
811824
run: |
812825
docker run -v $PWD/:/github -v $HOME:$HOME otp \
@@ -848,10 +861,9 @@ jobs:
848861
fail-on: ${{ github.ref_type == 'tag' && '' || '' }} # 'violations,issues' }}
849862
sw-version: ${{ env.OTP_SBOM_VERSION }}
850863

851-
vendor-analysis:
852-
name: Vendor Dependency Analysis
864+
vendor-dependency-upload:
865+
name: Vendor Dependency Upload
853866
runs-on: ubuntu-latest
854-
if: github.event_name == 'push'
855867
needs:
856868
- sbom
857869
- pack
@@ -879,6 +891,7 @@ jobs:
879891
880892
# allows Dependabot to give us alert of the vendor libraries that use semantic versioning
881893
- name: Upload SBOM to Github Dependency API
894+
if: github.event_name == 'push'
882895
uses: advanced-security/spdx-dependency-submission-action@5530bab9ee4bbe66420ce8280624036c77f89746 # ratchet:advanced-security/[email protected]
883896

884897
## If this is an "OTP-*" tag that has been pushed we do some release work

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

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,29 +64,26 @@ jobs:
6464
with:
6565
ref: ${{ matrix.type }}
6666

67+
# this is a call to a workflow_dispatch ref=master is important because
68+
# using ref={{matrix.type}} would trigger the workflow
69+
# reusable-vendor-vulnerability-scanner.yml in that ref/branch. since
70+
# there is no such files in maint-25, maint-26, etc, the result would
71+
# ignore the vulnerability scanning for those branches.
72+
#
73+
# During manual testing, triggering this event before the pack job (in
74+
# main.yml) is finished will result in an error, due to not being able to
75+
# fetch from the cache. In the scheduled case (common case), there should
76+
# be already a cache build and this is not an issue.
77+
#
6778
- name: Trigger Vulnerability Scanning
6879
env:
6980
GH_TOKEN: ${{ github.token }}
70-
if: ${{ hashFiles('.github/workflows/osv-scanner-scheduled.yml') != '' }}
81+
REPO : erlang/otp # in testing cases, this is your fork, e.g., kikofernandez/otp
82+
if: ${{ hashFiles('.github/workflows/reusable-vendor-vulnerability-scanner.yml') != '' }}
7183
run: |
7284
gh api \
7385
--method POST \
7486
-H "Accept: application/vnd.github+json" \
7587
-H "X-GitHub-Api-Version: 2022-11-28" \
76-
/repos/${{ github.repository }}/actions/workflows/osv-scanner-scheduled.yml/dispatches \
77-
-f "ref=${{ matrix.type }}"
78-
79-
scan-pr:
80-
# run-scheduled-scan triggers this job
81-
# PRs and pushes trigger this job
82-
if: github.event_name != 'schedule'
83-
permissions:
84-
# Require writing security events to upload SARIF file to security tab
85-
security-events: write
86-
# Required to upload SARIF file to CodeQL.
87-
# See: https://github.com/github/codeql-action/issues/2117
88-
actions: read
89-
contents: read
90-
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e69cc6c86b31f1e7e23935bbe7031b50e51082de" # ratchet:google/osv-scanner-action/.github/workflows/[email protected]
91-
with:
92-
upload-sarif: ${{ github.repository == 'erlang/otp' }}
88+
/repos/${{ env.REPO }}/actions/workflows/reusable-vendor-vulnerability-scanner.yml/dispatches \
89+
-f "ref=master" -f "inputs[version]=${{ matrix.type }}"

0 commit comments

Comments
 (0)