bitwarden_workflow_linter 1.3.6__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bitwarden_workflow_linter/__about__.py +1 -1
- bitwarden_workflow_linter/default_settings.yaml +6 -0
- bitwarden_workflow_linter/rules/check_blocked_domains.py +180 -0
- bitwarden_workflow_linter/utils.py +4 -0
- {bitwarden_workflow_linter-1.3.6.dist-info → bitwarden_workflow_linter-1.4.0.dist-info}/METADATA +1 -1
- {bitwarden_workflow_linter-1.3.6.dist-info → bitwarden_workflow_linter-1.4.0.dist-info}/RECORD +9 -8
- {bitwarden_workflow_linter-1.3.6.dist-info → bitwarden_workflow_linter-1.4.0.dist-info}/WHEEL +0 -0
- {bitwarden_workflow_linter-1.3.6.dist-info → bitwarden_workflow_linter-1.4.0.dist-info}/entry_points.txt +0 -0
- {bitwarden_workflow_linter-1.3.6.dist-info → bitwarden_workflow_linter-1.4.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -19,6 +19,12 @@ enabled_rules:
|
|
19
19
|
level: error
|
20
20
|
- id: bitwarden_workflow_linter.rules.permissions_exist.RulePermissionsExist
|
21
21
|
level: error
|
22
|
+
- id: bitwarden_workflow_linter.rules.check_blocked_domains.RuleCheckBlockedDomains
|
23
|
+
level: warning
|
22
24
|
|
23
25
|
approved_actions_path: default_actions.json
|
24
26
|
default_branch: main
|
27
|
+
|
28
|
+
# List of domains that should trigger an error if found in workflows
|
29
|
+
blocked_domains:
|
30
|
+
- ghrc.io
|
@@ -0,0 +1,180 @@
|
|
1
|
+
"""A Rule to check for blocked/malicious domains in workflow content."""
|
2
|
+
|
3
|
+
import re
|
4
|
+
from typing import List, Optional, Tuple, Union
|
5
|
+
|
6
|
+
from ..models.job import Job
|
7
|
+
from ..models.step import Step
|
8
|
+
from ..models.workflow import Workflow
|
9
|
+
from ..rule import Rule
|
10
|
+
from ..utils import LintLevels, Settings
|
11
|
+
|
12
|
+
|
13
|
+
class RuleCheckBlockedDomains(Rule):
|
14
|
+
"""Rule to detect blocked or malicious domains in workflow content.
|
15
|
+
|
16
|
+
This rule scans workflow content (URLs, scripts, commands) for domains
|
17
|
+
that have been flagged as blocked or potentially malicious. It helps
|
18
|
+
prevent supply chain attacks and unauthorized external dependencies.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.ERROR) -> None:
|
22
|
+
"""Constructor for RuleCheckBlockedDomains.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
settings:
|
26
|
+
A Settings object that contains any default, overridden, or custom settings
|
27
|
+
required anywhere in the application.
|
28
|
+
lint_level:
|
29
|
+
The LintLevels enum value to determine the severity of the rule.
|
30
|
+
"""
|
31
|
+
self.on_fail = lint_level
|
32
|
+
self.compatibility = [Workflow, Job, Step]
|
33
|
+
self.settings = settings
|
34
|
+
|
35
|
+
# Get blocked domains from settings, use defaults if none provided
|
36
|
+
self.blocked_domains = settings.blocked_domains if settings else []
|
37
|
+
|
38
|
+
def extract_domains_from_text(self, text: str) -> List[str]:
|
39
|
+
"""Extract domain names from text content.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
text: The text to scan for domains
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
List of domain names found in the text
|
46
|
+
"""
|
47
|
+
if not text:
|
48
|
+
return []
|
49
|
+
|
50
|
+
# Pattern to match domain names in various contexts
|
51
|
+
# Breaking down the regex:
|
52
|
+
# (?:https?://)? - Optional protocol (http:// or https://)
|
53
|
+
# (?:www\.)? - Optional www. prefix
|
54
|
+
# ([a-zA-Z0-9] - Start of capturing group: first character (alphanumeric)
|
55
|
+
# (?:[a-zA-Z0-9-]{0,61} - Non-capturing group: 0-61 alphanumeric or hyphen chars
|
56
|
+
# [a-zA-Z0-9])? - End with alphanumeric (ensures no trailing hyphen)
|
57
|
+
# \.)+ - Followed by dot, repeated (for subdomains)
|
58
|
+
# [a-zA-Z]{2,} - Top-level domain (2+ letters)
|
59
|
+
# (?:/[^\s]*)? - Optional path (slash followed by non-whitespace chars)
|
60
|
+
domain_pattern = r'(?:https?://)?(?:www\.)?([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:/[^\s]*)?'
|
61
|
+
|
62
|
+
domains = set()
|
63
|
+
|
64
|
+
# Find all matches and extract the domain part
|
65
|
+
for match in re.finditer(domain_pattern, text, re.IGNORECASE):
|
66
|
+
full_match = match.group(0)
|
67
|
+
# Clean up the domain (remove protocol, www, path)
|
68
|
+
domain = re.sub(r'^https?://', '', full_match)
|
69
|
+
domain = re.sub(r'^www\.', '', domain)
|
70
|
+
domain = re.sub(r'/.*$', '', domain) # Remove path
|
71
|
+
domain = domain.lower().strip()
|
72
|
+
|
73
|
+
if domain and '.' in domain:
|
74
|
+
domains.add(domain)
|
75
|
+
|
76
|
+
return list(domains)
|
77
|
+
|
78
|
+
def check_blocked_domains(self, text: str) -> Tuple[bool, List[str]]:
|
79
|
+
"""Check if text contains any blocked domains.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
text: The text to check for blocked domains
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
Tuple of (is_clean, list_of_found_blocked_domains)
|
86
|
+
"""
|
87
|
+
if not text:
|
88
|
+
return True, []
|
89
|
+
|
90
|
+
domains = self.extract_domains_from_text(text)
|
91
|
+
blocked_found = []
|
92
|
+
|
93
|
+
for domain in domains:
|
94
|
+
for blocked_domain in self.blocked_domains:
|
95
|
+
blocked_domain_lc = blocked_domain.lower().strip()
|
96
|
+
domain_lc = domain.lower().strip()
|
97
|
+
# Check for exact match or subdomain match
|
98
|
+
if domain_lc == blocked_domain_lc or domain_lc.endswith('.' + blocked_domain_lc):
|
99
|
+
blocked_found.append(domain)
|
100
|
+
|
101
|
+
return len(blocked_found) == 0, blocked_found
|
102
|
+
|
103
|
+
def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]:
|
104
|
+
"""Check the workflow object for blocked domains.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
obj: The workflow object (Workflow, Job, or Step) to check
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Tuple of (success, error_message)
|
111
|
+
"""
|
112
|
+
blocked_domains_found = []
|
113
|
+
|
114
|
+
if isinstance(obj, Workflow):
|
115
|
+
# Check workflow-level content
|
116
|
+
if obj.name:
|
117
|
+
is_clean, domains = self.check_blocked_domains(obj.name)
|
118
|
+
if not is_clean:
|
119
|
+
blocked_domains_found.extend([(f"workflow name", domain) for domain in domains])
|
120
|
+
|
121
|
+
elif isinstance(obj, Job):
|
122
|
+
# Check job-level content
|
123
|
+
if obj.name:
|
124
|
+
is_clean, domains = self.check_blocked_domains(obj.name)
|
125
|
+
if not is_clean:
|
126
|
+
blocked_domains_found.extend([(f"job name", domain) for domain in domains])
|
127
|
+
|
128
|
+
if obj.uses:
|
129
|
+
is_clean, domains = self.check_blocked_domains(obj.uses)
|
130
|
+
if not is_clean:
|
131
|
+
blocked_domains_found.extend([(f"job uses", domain) for domain in domains])
|
132
|
+
|
133
|
+
# Check environment variables
|
134
|
+
if obj.env:
|
135
|
+
for key, value in obj.env.items():
|
136
|
+
if isinstance(value, str):
|
137
|
+
is_clean, domains = self.check_blocked_domains(value)
|
138
|
+
if not is_clean:
|
139
|
+
blocked_domains_found.extend([(f"job env '{key}'", domain) for domain in domains])
|
140
|
+
|
141
|
+
elif isinstance(obj, Step):
|
142
|
+
# Check step-level content
|
143
|
+
if obj.name:
|
144
|
+
is_clean, domains = self.check_blocked_domains(obj.name)
|
145
|
+
if not is_clean:
|
146
|
+
blocked_domains_found.extend([(f"step name", domain) for domain in domains])
|
147
|
+
|
148
|
+
if obj.uses:
|
149
|
+
is_clean, domains = self.check_blocked_domains(obj.uses)
|
150
|
+
if not is_clean:
|
151
|
+
blocked_domains_found.extend([(f"step uses", domain) for domain in domains])
|
152
|
+
|
153
|
+
if obj.run:
|
154
|
+
is_clean, domains = self.check_blocked_domains(obj.run)
|
155
|
+
if not is_clean:
|
156
|
+
blocked_domains_found.extend([(f"step run command", domain) for domain in domains])
|
157
|
+
|
158
|
+
# Check environment variables
|
159
|
+
if obj.env:
|
160
|
+
for key, value in obj.env.items():
|
161
|
+
if isinstance(value, str):
|
162
|
+
is_clean, domains = self.check_blocked_domains(value)
|
163
|
+
if not is_clean:
|
164
|
+
blocked_domains_found.extend([(f"step env '{key}'", domain) for domain in domains])
|
165
|
+
|
166
|
+
# Check step with parameters
|
167
|
+
if obj.uses_with:
|
168
|
+
for key, value in obj.uses_with.items():
|
169
|
+
if isinstance(value, str):
|
170
|
+
is_clean, domains = self.check_blocked_domains(value)
|
171
|
+
if not is_clean:
|
172
|
+
blocked_domains_found.extend([(f"step with '{key}'", domain) for domain in domains])
|
173
|
+
|
174
|
+
if blocked_domains_found:
|
175
|
+
error_details = []
|
176
|
+
for location, domain in blocked_domains_found:
|
177
|
+
error_details.append(f"Found blocked domain '{domain}' in {location}")
|
178
|
+
return False, "; ".join(error_details)
|
179
|
+
|
180
|
+
return True, ""
|
@@ -114,6 +114,7 @@ class Settings:
|
|
114
114
|
approved_actions: dict[str, Action]
|
115
115
|
actionlint_version: str
|
116
116
|
default_branch: Optional[str]
|
117
|
+
blocked_domains: Optional[list[str]]
|
117
118
|
|
118
119
|
def __init__(
|
119
120
|
self,
|
@@ -121,6 +122,7 @@ class Settings:
|
|
121
122
|
approved_actions: Optional[dict[str, dict[str, str]]] = None,
|
122
123
|
actionlint_version: Optional[str] = None,
|
123
124
|
default_branch: Optional[str] = None,
|
125
|
+
blocked_domains: Optional[list[str]] = None,
|
124
126
|
) -> None:
|
125
127
|
"""Settings object that can be overridden in settings.py.
|
126
128
|
|
@@ -147,6 +149,7 @@ class Settings:
|
|
147
149
|
name: Action(**action) for name, action in approved_actions.items()
|
148
150
|
}
|
149
151
|
self.default_branch = default_branch
|
152
|
+
self.blocked_domains = blocked_domains or []
|
150
153
|
|
151
154
|
@staticmethod
|
152
155
|
def factory() -> SettingsFromFactory:
|
@@ -201,4 +204,5 @@ class Settings:
|
|
201
204
|
approved_actions=settings["approved_actions"],
|
202
205
|
actionlint_version=actionlint_version,
|
203
206
|
default_branch=default_branch,
|
207
|
+
blocked_domains=settings.get("blocked_domains", []),
|
204
208
|
)
|
{bitwarden_workflow_linter-1.3.6.dist-info → bitwarden_workflow_linter-1.4.0.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bitwarden_workflow_linter
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.4.0
|
4
4
|
Summary: Custom GitHub Action Workflow Linter
|
5
5
|
Project-URL: Homepage, https://github.com/bitwarden/workflow-linter
|
6
6
|
Project-URL: Issues, https://github.com/bitwarden/workflow-linter/issues
|
{bitwarden_workflow_linter-1.3.6.dist-info → bitwarden_workflow_linter-1.4.0.dist-info}/RECORD
RENAMED
@@ -1,19 +1,20 @@
|
|
1
|
-
bitwarden_workflow_linter/__about__.py,sha256
|
1
|
+
bitwarden_workflow_linter/__about__.py,sha256=-Y6k93jwyGvISIX3gB34noQVqGX1AWpSjfpcbkH39uk,59
|
2
2
|
bitwarden_workflow_linter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
bitwarden_workflow_linter/actionlint_version.yaml,sha256=CKhiDwaDBNCExOHTlcpiavfEgf01uG_tTPrgLRaj6_k,28
|
4
4
|
bitwarden_workflow_linter/actions.py,sha256=LAn3yQeMMmCOvJWeTn3dE1U2nyEJqIBMwESq3TtY9hE,9069
|
5
5
|
bitwarden_workflow_linter/cli.py,sha256=v2XNncIBscGyH691Ngeew6qSVRJQm1r3RwmpKoXooW8,1812
|
6
6
|
bitwarden_workflow_linter/default_actions.json,sha256=KWubDc6qWgmdiMC4Z9zVl2Mu5GQVIi2wIdljuZvsM9g,14562
|
7
|
-
bitwarden_workflow_linter/default_settings.yaml,sha256=
|
7
|
+
bitwarden_workflow_linter/default_settings.yaml,sha256=shXZ7-0v2o3rrV9WwW3pt4UUlKh1al5fqJi8AuRiB34,1267
|
8
8
|
bitwarden_workflow_linter/lint.py,sha256=ei66eVupfIgNj_-68CSwLfstOe66noEYNmAV2kzn8NA,6348
|
9
9
|
bitwarden_workflow_linter/load.py,sha256=FWxotIlB0vyKzrVw87sOx3qdRiJG_0hVHRbbLXZY4Sc,5553
|
10
10
|
bitwarden_workflow_linter/rule.py,sha256=Qb60JiUDAWN3ayrMGoSbbDCSFmw-ql8djzAkxISaob4,3250
|
11
|
-
bitwarden_workflow_linter/utils.py,sha256=
|
11
|
+
bitwarden_workflow_linter/utils.py,sha256=93PS9zVhniHZVbBnCbvUyIUJj2gG3RJFJa6_vmp5Mr0,6035
|
12
12
|
bitwarden_workflow_linter/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
bitwarden_workflow_linter/models/job.py,sha256=oqFq8A4JGQplBlaDjUUFV9kWT5rh9A0V6FYGf0IaGg0,2553
|
14
14
|
bitwarden_workflow_linter/models/step.py,sha256=j81iWYWcNI9x55n1MOR0N6ogKaQ_4-CKu9LnI_fwEOE,1814
|
15
15
|
bitwarden_workflow_linter/models/workflow.py,sha256=lIgGI2cDwC2lTOM-k3fqKgceLdSJ6vhTLCAhaeoD-fc,1645
|
16
16
|
bitwarden_workflow_linter/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
bitwarden_workflow_linter/rules/check_blocked_domains.py,sha256=v-sbwqRl3x2mupKLvnqFECOx_LPVbaAkKD_icT_srzE,7771
|
17
18
|
bitwarden_workflow_linter/rules/check_pr_target.py,sha256=HXvOvDZeZ7OPtIs_pqcXlxffUWqEsCmvtuz9_APxXf8,3507
|
18
19
|
bitwarden_workflow_linter/rules/job_environment_prefix.py,sha256=bdE8l4B5DQiCFVmblXTs4ptsHPGvjhJrR5ONo2kRY2U,2757
|
19
20
|
bitwarden_workflow_linter/rules/name_capitalized.py,sha256=lGHPi_Ix0DVSzGEdrUm2vAEQD4qQ8dxU1hddsCdqA2w,2126
|
@@ -24,8 +25,8 @@ bitwarden_workflow_linter/rules/run_actionlint.py,sha256=m6SaejtkUz704exAiq_ti0d
|
|
24
25
|
bitwarden_workflow_linter/rules/step_approved.py,sha256=4pUCrOlWomo43bwGBunORphv1RJzc3spRKgZ4VLtDS0,3304
|
25
26
|
bitwarden_workflow_linter/rules/step_pinned.py,sha256=MagV8LNdgRKyncmSdH9V-TlIcsdjzoDHDWqovzWon9E,3559
|
26
27
|
bitwarden_workflow_linter/rules/underscore_outputs.py,sha256=LoCsDN_EfQ8H9n5BfZ5xCe7BeHqJGPMcV0vo1c9YJcw,4275
|
27
|
-
bitwarden_workflow_linter-1.
|
28
|
-
bitwarden_workflow_linter-1.
|
29
|
-
bitwarden_workflow_linter-1.
|
30
|
-
bitwarden_workflow_linter-1.
|
31
|
-
bitwarden_workflow_linter-1.
|
28
|
+
bitwarden_workflow_linter-1.4.0.dist-info/METADATA,sha256=lXVfByPjXiRaPaxQdT8mCX3Jz5URBzAqdnHa3ImUGf0,10227
|
29
|
+
bitwarden_workflow_linter-1.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
30
|
+
bitwarden_workflow_linter-1.4.0.dist-info/entry_points.txt,sha256=SA_yF9CwL4VMUvdcmCd7k9rjsQNzfeOUBuDnMnaO8QQ,60
|
31
|
+
bitwarden_workflow_linter-1.4.0.dist-info/licenses/LICENSE.txt,sha256=uY-7N9tbI7xc_c0WeTIGpacSCnsB91N05eCIg3bkaRw,35140
|
32
|
+
bitwarden_workflow_linter-1.4.0.dist-info/RECORD,,
|
{bitwarden_workflow_linter-1.3.6.dist-info → bitwarden_workflow_linter-1.4.0.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|