Skip to content

Commit 71408a5

Browse files
authored
Merge pull request #2024 from ziadhany/github_poc
Add GitHub PoC collector
2 parents e36188b + ab162f4 commit 71408a5

8 files changed

Lines changed: 595 additions & 0 deletions

File tree

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
computer_package_version_rank as compute_version_rank_v2,
2727
)
2828
from vulnerabilities.pipelines.v2_improvers import enhance_with_exploitdb as exploitdb_v2
29+
from vulnerabilities.pipelines.v2_improvers import enhance_with_github_poc
2930
from vulnerabilities.pipelines.v2_improvers import enhance_with_kev as enhance_with_kev_v2
3031
from vulnerabilities.pipelines.v2_improvers import (
3132
enhance_with_metasploit as enhance_with_metasploit_v2,
@@ -77,5 +78,6 @@
7778
group_advisories_for_packages.GroupAdvisoriesForPackages,
7879
compute_advisory_todo_v2.ComputeToDo,
7980
reference_collect_commits.CollectReferencesFixCommitsPipeline,
81+
enhance_with_github_poc.GithubPocsImproverPipeline,
8082
]
8183
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Generated by Django 5.2.11 on 2026-05-15 01:38
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("vulnerabilities", "0128_advisoryreference_archive_url"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="AdvisoryPOC",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
21+
),
22+
),
23+
(
24+
"created_at",
25+
models.DateTimeField(
26+
blank=True,
27+
help_text="The date and time when this POC was created.",
28+
null=True,
29+
),
30+
),
31+
(
32+
"updated_at",
33+
models.DateTimeField(
34+
blank=True,
35+
help_text="The date and time when this POC was last updated.",
36+
null=True,
37+
),
38+
),
39+
(
40+
"url",
41+
models.URLField(
42+
help_text="The URL of the PoC, such as a repository or resource link."
43+
),
44+
),
45+
(
46+
"is_confirmed",
47+
models.BooleanField(
48+
default=False,
49+
help_text="Indicates whether this POC has been verified or confirmed.",
50+
),
51+
),
52+
(
53+
"advisory",
54+
models.ForeignKey(
55+
on_delete=django.db.models.deletion.CASCADE,
56+
related_name="pocs",
57+
to="vulnerabilities.advisoryv2",
58+
),
59+
),
60+
],
61+
),
62+
]

vulnerabilities/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3864,3 +3864,29 @@ class GroupedAdvisory(NamedTuple):
38643864
weighted_severity: Optional[float]
38653865
exploitability: Optional[float]
38663866
risk_score: Optional[float]
3867+
3868+
3869+
class AdvisoryPOC(models.Model):
3870+
"""
3871+
An AdvisoryPOC (Proof of Concept) demonstrating how a vulnerability related to an advisory can be exploited.
3872+
"""
3873+
3874+
advisory = models.ForeignKey(
3875+
"AdvisoryV2",
3876+
related_name="pocs",
3877+
on_delete=models.CASCADE,
3878+
)
3879+
3880+
created_at = models.DateTimeField(
3881+
null=True, blank=True, help_text="The date and time when this POC was created."
3882+
)
3883+
3884+
updated_at = models.DateTimeField(
3885+
null=True, blank=True, help_text="The date and time when this POC was last updated."
3886+
)
3887+
3888+
url = models.URLField(help_text="The URL of the PoC, such as a repository or resource link.")
3889+
3890+
is_confirmed = models.BooleanField(
3891+
default=False, help_text="Indicates whether this POC has been verified or confirmed."
3892+
)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import json
11+
from pathlib import Path
12+
13+
from aboutcode.pipeline import LoopProgress
14+
from fetchcode.vcs import fetch_via_vcs
15+
16+
from vulnerabilities.models import AdvisoryAlias
17+
from vulnerabilities.models import AdvisoryPOC
18+
from vulnerabilities.models import AdvisoryV2
19+
from vulnerabilities.pipelines import VulnerableCodePipeline
20+
21+
22+
class GithubPocsImproverPipeline(VulnerableCodePipeline):
23+
"""
24+
Pipeline to Collect an exploit-PoCs repository, parse exploit JSON files,
25+
match them to advisories via aliases, and update/create POC records.
26+
"""
27+
28+
pipeline_id = "enhance_with_github_poc"
29+
repo_url = "git+https://github.com/nomi-sec/PoC-in-GitHub"
30+
31+
@classmethod
32+
def steps(cls):
33+
return (
34+
cls.clone_repo,
35+
cls.collect_and_store_exploits,
36+
cls.clean_downloads,
37+
)
38+
39+
def clone_repo(self):
40+
self.log(f"Cloning `{self.repo_url}`")
41+
self.vcs_response = fetch_via_vcs(self.repo_url)
42+
43+
def collect_and_store_exploits(self):
44+
"""
45+
Parse PoC JSON files, match them to advisories via aliases,
46+
and create or update related exploit records.
47+
"""
48+
49+
base_directory = Path(self.vcs_response.dest_dir)
50+
json_files = list(base_directory.rglob("**/*.json"))
51+
exploits_count = len(json_files)
52+
self.log(f"Enhancing the vulnerability with {exploits_count:,d} exploit records")
53+
progress = LoopProgress(total_iterations=exploits_count, logger=self.log)
54+
for file_path in progress.iter(json_files):
55+
with open(file_path, "r") as f:
56+
try:
57+
exploits_data = json.load(f)
58+
except json.JSONDecodeError:
59+
self.log(f"Invalid JSON in {file_path}, skipping.")
60+
continue
61+
62+
filename = file_path.stem.strip()
63+
64+
advisories = set()
65+
try:
66+
if alias := AdvisoryAlias.objects.get(alias=filename):
67+
for adv in alias.advisories.all():
68+
advisories.add(adv)
69+
else:
70+
advs = AdvisoryV2.objects.filter(advisory_id=filename).latest_per_avid()
71+
for adv in advs:
72+
advisories.add(adv)
73+
except AdvisoryAlias.DoesNotExist:
74+
self.log(f"Advisory {filename} not found.")
75+
continue
76+
77+
for advisory in advisories:
78+
for exploit_data in exploits_data:
79+
exploit_repo_url = exploit_data.get("html_url")
80+
if not exploit_repo_url:
81+
continue
82+
83+
AdvisoryPOC.objects.update_or_create(
84+
advisory=advisory,
85+
url=exploit_repo_url,
86+
defaults={
87+
"created_at": exploit_data.get("created_at"),
88+
"updated_at": exploit_data.get("updated_at"),
89+
},
90+
)
91+
92+
self.log(f"Successfully added {exploits_count:,d} poc exploit advisory")
93+
94+
def clean_downloads(self):
95+
if self.vcs_response:
96+
self.log(f"Removing cloned repository")
97+
self.vcs_response.delete()
98+
99+
def on_failure(self):
100+
self.clean_downloads()
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import os
11+
from datetime import datetime
12+
from unittest import mock
13+
from unittest.mock import MagicMock
14+
15+
import pytest
16+
17+
from vulnerabilities.models import AdvisoryAlias
18+
from vulnerabilities.models import AdvisoryPOC
19+
from vulnerabilities.models import AdvisoryV2
20+
from vulnerabilities.pipelines.v2_improvers.enhance_with_github_poc import (
21+
GithubPocsImproverPipeline,
22+
)
23+
24+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
25+
26+
TEST_REPO_DIR = os.path.join(BASE_DIR, "../../test_data/github_poc")
27+
28+
29+
@pytest.mark.django_db
30+
@mock.patch("vulnerabilities.pipelines.v2_improvers.enhance_with_github_poc.fetch_via_vcs")
31+
def test_github_poc_db_improver(mock_fetch_via_vcs):
32+
mock_vcs = MagicMock()
33+
mock_vcs.dest_dir = TEST_REPO_DIR
34+
mock_vcs.delete = MagicMock()
35+
mock_fetch_via_vcs.return_value = mock_vcs
36+
37+
adv1 = AdvisoryV2.objects.create(
38+
advisory_id="VCIO-123-0001",
39+
datasource_id="ds",
40+
avid="ds/VCIO-123-0001",
41+
unique_content_id="sgsdg45",
42+
url="https://test.com",
43+
date_collected=datetime.now(),
44+
)
45+
adv2 = AdvisoryV2.objects.create(
46+
advisory_id="VCIO-123-1002",
47+
datasource_id="ds",
48+
avid="ds/VCIO-123-1002",
49+
unique_content_id="6hd4d6f",
50+
url="https://test.com",
51+
date_collected=datetime.now(),
52+
)
53+
adv3 = AdvisoryV2.objects.create(
54+
advisory_id="VCIO-123-1003",
55+
datasource_id="ds",
56+
avid="ds/VCIO-123-1003",
57+
unique_content_id="sd6h4sh",
58+
url="https://test.com",
59+
date_collected=datetime.now(),
60+
)
61+
62+
alias1 = AdvisoryAlias.objects.create(alias="CVE-2022-0236")
63+
alias2 = AdvisoryAlias.objects.create(alias="CVE-2025-0108")
64+
alias3 = AdvisoryAlias.objects.create(alias="CVE-2025-0309")
65+
adv1.aliases.add(alias1)
66+
adv2.aliases.add(alias2)
67+
adv3.aliases.add(alias3)
68+
69+
improver = GithubPocsImproverPipeline()
70+
improver.execute()
71+
72+
assert len(AdvisoryPOC.objects.all()) == 10
73+
exploit1 = AdvisoryPOC.objects.get(
74+
url="https://github.com/iSee857/CVE-2025-0108-PoC",
75+
is_confirmed=False,
76+
)
77+
exploit2 = AdvisoryPOC.objects.get(
78+
url="https://github.com/FOLKS-iwd/CVE-2025-0108-PoC", advisory=adv2
79+
)
80+
exploit3 = AdvisoryPOC.objects.get(
81+
url="https://github.com/B1ack4sh/Blackash-CVE-2025-0108",
82+
)
83+
assert exploit1.url == "https://github.com/iSee857/CVE-2025-0108-PoC"
84+
assert str(exploit1.created_at) == "2025-02-13 06:39:25+00:00"
85+
assert str(exploit2.updated_at) == "2025-04-28 07:22:48+00:00"
86+
assert exploit2.url == "https://github.com/FOLKS-iwd/CVE-2025-0108-PoC"
87+
assert exploit3.url == "https://github.com/B1ack4sh/Blackash-CVE-2025-0108"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
[
2+
{
3+
"id": 448514056,
4+
"name": "CVE-2022-0236",
5+
"full_name": "qurbat\/CVE-2022-0236",
6+
"owner": {
7+
"login": "qurbat",
8+
"id": 37518297,
9+
"avatar_url": "https:\/\/avatars.githubusercontent.com\/u\/37518297?v=4",
10+
"html_url": "https:\/\/github.com\/qurbat",
11+
"user_view_type": "public"
12+
},
13+
"html_url": "https:\/\/github.com\/qurbat\/CVE-2022-0236",
14+
"description": "Proof of concept for unauthenticated sensitive data disclosure affecting the wp-import-export WordPress plugin (CVE-2022-0236)",
15+
"fork": false,
16+
"created_at": "2022-01-16T09:52:28Z",
17+
"updated_at": "2023-01-28T03:56:57Z",
18+
"pushed_at": "2022-01-18T17:14:53Z",
19+
"stargazers_count": 3,
20+
"watchers_count": 3,
21+
"has_discussions": false,
22+
"forks_count": 3,
23+
"allow_forking": true,
24+
"is_template": false,
25+
"web_commit_signoff_required": false,
26+
"topics": [
27+
"wordpress-security"
28+
],
29+
"visibility": "public",
30+
"forks": 3,
31+
"watchers": 3,
32+
"score": 0,
33+
"subscribers_count": 1
34+
},
35+
{
36+
"id": 448893968,
37+
"name": "CVE-2022-0236",
38+
"full_name": "xiska62314\/CVE-2022-0236",
39+
"owner": {
40+
"login": "xiska62314",
41+
"id": 97891523,
42+
"avatar_url": "https:\/\/avatars.githubusercontent.com\/u\/97891523?v=4",
43+
"html_url": "https:\/\/github.com\/xiska62314",
44+
"user_view_type": "public"
45+
},
46+
"html_url": "https:\/\/github.com\/xiska62314\/CVE-2022-0236",
47+
"description": "CVE-2022-0236",
48+
"fork": false,
49+
"created_at": "2022-01-17T12:56:19Z",
50+
"updated_at": "2022-01-17T12:56:19Z",
51+
"pushed_at": "2022-01-17T12:56:20Z",
52+
"stargazers_count": 0,
53+
"watchers_count": 0,
54+
"has_discussions": false,
55+
"forks_count": 0,
56+
"allow_forking": true,
57+
"is_template": false,
58+
"web_commit_signoff_required": false,
59+
"topics": [],
60+
"visibility": "public",
61+
"forks": 0,
62+
"watchers": 0,
63+
"score": 0,
64+
"subscribers_count": 1
65+
}
66+
]

0 commit comments

Comments
 (0)