Skip to content

Commit f5d965d

Browse files
committed
Add new GIthub Action ecosystem
1 parent 88d0c80 commit f5d965d

File tree

8 files changed

+111
-0
lines changed

8 files changed

+111
-0
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,21 @@ Metadata heuristics:
155155
| typosquatting | Identify packages that are named closely to an highly popular package |
156156

157157

158+
### GitHub Action
159+
160+
Source code heuristics:
161+
162+
| **Heuristic** | **Description** |
163+
|:-------------:|:---------------:|
164+
| npm-serialize-environment | Identify when a package serializes 'process.env' to exfiltrate environment variables |
165+
| npm-obfuscation | Identify when a package uses a common obfuscation method often used by malware |
166+
| npm-silent-process-execution | Identify when a package silently executes an executable |
167+
| shady-links | Identify when a package contains an URL to a domain with a suspicious extension |
168+
| npm-exec-base64 | Identify when a package dynamically executes code through 'eval' |
169+
| npm-install-script | Identify when a package has a pre or post-install script automatically running commands |
170+
| npm-steganography | Identify when a package retrieves hidden data from an image and executes it |
171+
| npm-dll-hijacking | Identifies when a malicious package manipulates a trusted application into loading a malicious DLL |
172+
| npm-exfiltrate-sensitive-data | Identify when a package reads and exfiltrates sensitive data from the local system |
158173
<!-- END_RULE_LIST -->
159174

160175
## Custom Rules

guarddog/analyzer/metadata/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from guarddog.analyzer.metadata.npm import NPM_METADATA_RULES
33
from guarddog.analyzer.metadata.pypi import PYPI_METADATA_RULES
44
from guarddog.analyzer.metadata.go import GO_METADATA_RULES
5+
from guarddog.analyzer.metadata.github_action import GITHUB_ACTION_METADATA_RULES
56
from guarddog.ecosystems import ECOSYSTEM
67

78

@@ -13,3 +14,5 @@ def get_metadata_detectors(ecosystem: ECOSYSTEM) -> dict[str, Detector]:
1314
return NPM_METADATA_RULES
1415
case ECOSYSTEM.GO:
1516
return GO_METADATA_RULES
17+
case ECOSYSTEM.GITHUB_ACTION:
18+
return GITHUB_ACTION_METADATA_RULES
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import Type
2+
3+
from guarddog.analyzer.metadata import Detector
4+
5+
GITHUB_ACTION_METADATA_RULES = {}
6+
7+
classes: list[Type[Detector]] = []
8+
9+
for detectorClass in classes:
10+
detectorInstance = detectorClass() # type: ignore
11+
GITHUB_ACTION_METADATA_RULES[detectorInstance.get_name()] = detectorInstance

guarddog/analyzer/sourcecode/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def get_sourcecode_rules(
5151
ecosystem: The ecosystem to filter for if rules are ecosystem specific
5252
kind: The kind of rule to filter for
5353
"""
54+
if ecosystem == ECOSYSTEM.GITHUB_ACTION:
55+
ecosystem = ECOSYSTEM.NPM
56+
5457
for rule in SOURCECODE_RULES:
5558
if kind and not isinstance(rule, kind):
5659
continue

guarddog/ecosystems.py

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class ECOSYSTEM(Enum):
55
PYPI = "pypi"
66
NPM = "npm"
77
GO = "go"
8+
GITHUB_ACTION = "github-action"
89

910

1011
def get_friendly_name(ecosystem: ECOSYSTEM) -> str:
@@ -15,5 +16,7 @@ def get_friendly_name(ecosystem: ECOSYSTEM) -> str:
1516
return "npm"
1617
case ECOSYSTEM.GO:
1718
return "go"
19+
case ECOSYSTEM.GITHUB_ACTION:
20+
return "GitHub Action"
1821
case _:
1922
return ecosystem.value

guarddog/scanners/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .pypi_project_scanner import PypiRequirementsScanner
77
from .go_package_scanner import GoModuleScanner
88
from .go_project_scanner import GoDependenciesScanner
9+
from .github_action_scanner import GithubActionScanner
910
from .scanner import PackageScanner, ProjectScanner
1011
from ..ecosystems import ECOSYSTEM
1112

@@ -29,6 +30,8 @@ def get_package_scanner(ecosystem: ECOSYSTEM) -> Optional[PackageScanner]:
2930
return NPMPackageScanner()
3031
case ECOSYSTEM.GO:
3132
return GoModuleScanner()
33+
case ECOSYSTEM.GITHUB_ACTION:
34+
return GithubActionScanner()
3235
return None
3336

3437

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import logging
2+
import os
3+
import pathlib
4+
import typing
5+
from urllib.parse import urlparse
6+
7+
from guarddog.analyzer.analyzer import Analyzer
8+
from guarddog.ecosystems import ECOSYSTEM
9+
from guarddog.scanners.scanner import PackageScanner
10+
11+
log = logging.getLogger("guarddog")
12+
13+
14+
class GithubActionScanner(PackageScanner):
15+
def __init__(self) -> None:
16+
super().__init__(Analyzer(ECOSYSTEM.GITHUB_ACTION))
17+
18+
def download_and_get_package_info(self, directory: str, package_name: str, version=None) -> typing.Tuple[dict, str]:
19+
repo = self._get_repo(package_name)
20+
tarball_url = self._get_git_tarball_url(repo, version)
21+
22+
log.debug(f"Downloading GitHub Action source from {tarball_url}")
23+
24+
file_extension = pathlib.Path(tarball_url).suffix
25+
if file_extension == "":
26+
file_extension = ".zip"
27+
28+
zippath = os.path.join(directory, package_name.replace("/", "-") + file_extension)
29+
unzippedpath = zippath.removesuffix(file_extension)
30+
self.download_compressed(tarball_url, zippath, unzippedpath)
31+
32+
return {}, unzippedpath
33+
34+
def _get_repo(self, url: str) -> str:
35+
parsed_url = urlparse(url)
36+
37+
if parsed_url.hostname and parsed_url.hostname != "github.com":
38+
raise Exception(parsed_url)
39+
40+
path = parsed_url.path.removesuffix(".git").strip("/")
41+
42+
if path.count("/") != 1:
43+
raise Exception("Invalid GitHub repo URL: " + url)
44+
45+
return path
46+
47+
def _get_git_tarball_url(self, repo: str, version=None) -> str:
48+
if not version:
49+
return f"https://api.github.com/repos/{repo}/zipball"
50+
else:
51+
return f"https://github.com/{repo}/archive/refs/tags/{version}.zip"
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os.path
2+
import tempfile
3+
4+
import pytest
5+
6+
from guarddog.scanners import GithubActionScanner
7+
8+
9+
def test_download_and_get_github_action_by_url():
10+
scanner = GithubActionScanner()
11+
with tempfile.TemporaryDirectory() as tmpdirname:
12+
data, path = scanner.download_and_get_package_info(tmpdirname, "https://github.com/expressjs/express.git", "v5.0.0")
13+
assert not data
14+
assert os.path.exists(os.path.join(tmpdirname, "https:--github.com-expressjs-express.git", "express-5.0.0", "package.json"))
15+
16+
17+
def test_download_and_get_github_action_by_name():
18+
scanner = GithubActionScanner()
19+
with tempfile.TemporaryDirectory() as tmpdirname:
20+
data, path = scanner.download_and_get_package_info(tmpdirname, "expressjs/express", "v5.0.0")
21+
assert not data
22+
assert os.path.exists(os.path.join(tmpdirname, "expressjs-express", "express-5.0.0", "package.json"))

0 commit comments

Comments
 (0)