bitwarden_workflow_linter 1.3.5__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.
@@ -1,3 +1,3 @@
1
1
  """Metadata for Workflow Linter."""
2
2
 
3
- __version__ = "1.3.5"
3
+ __version__ = "1.4.0"
@@ -463,5 +463,10 @@
463
463
  "name": "yogevbd/enforce-label-action",
464
464
  "sha": "a3c219da6b8fa73f6ba62b68ff09c469b3a1c024",
465
465
  "version": "2.2.2"
466
+ },
467
+ "zizmorcore/zizmor-action": {
468
+ "name": "zizmorcore/zizmor-action",
469
+ "sha": "5ca5fc7a4779c5263a3ffa0e1f693009994446d1",
470
+ "version": "v0.1.2"
466
471
  }
467
472
  }
@@ -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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bitwarden_workflow_linter
3
- Version: 1.3.5
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
@@ -1,19 +1,20 @@
1
- bitwarden_workflow_linter/__about__.py,sha256=HtbtQa9qdNm2w7XisN1PYSnjhyrmSa7MdX08d4DRAfs,59
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
- bitwarden_workflow_linter/default_actions.json,sha256=VotDA4-K1a9RTVQfMAOIOVkqYEnXnlnfpAwjuj1tvTE,14406
7
- bitwarden_workflow_linter/default_settings.yaml,sha256=CYFWd_VwJtBlQuyMKGBqat-5odolWfARKsr08mldsgo,1059
6
+ bitwarden_workflow_linter/default_actions.json,sha256=KWubDc6qWgmdiMC4Z9zVl2Mu5GQVIi2wIdljuZvsM9g,14562
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=KV2Vo-hhNVRWOiIq_y-55li-noMt9F-FFgkJK-nUKJo,5823
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.3.5.dist-info/METADATA,sha256=phyCCSCAeEDb6H9aiXsUI1iaFoipysFP7nVSMkZLQOg,10227
28
- bitwarden_workflow_linter-1.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- bitwarden_workflow_linter-1.3.5.dist-info/entry_points.txt,sha256=SA_yF9CwL4VMUvdcmCd7k9rjsQNzfeOUBuDnMnaO8QQ,60
30
- bitwarden_workflow_linter-1.3.5.dist-info/licenses/LICENSE.txt,sha256=uY-7N9tbI7xc_c0WeTIGpacSCnsB91N05eCIg3bkaRw,35140
31
- bitwarden_workflow_linter-1.3.5.dist-info/RECORD,,
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,,