bitwarden_workflow_linter 0.5.6__tar.gz → 0.7.0__tar.gz

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.
Files changed (82) hide show
  1. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/workflows/enforce-labels.yml +1 -1
  2. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/PKG-INFO +17 -9
  3. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/README.md +16 -8
  4. bitwarden_workflow_linter-0.7.0/settings.yaml +21 -0
  5. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/__about__.py +1 -1
  6. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/default_actions.json +9 -4
  7. bitwarden_workflow_linter-0.7.0/src/bitwarden_workflow_linter/default_settings.yaml +21 -0
  8. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/load.py +23 -4
  9. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/models/job.py +9 -0
  10. bitwarden_workflow_linter-0.7.0/src/bitwarden_workflow_linter/rules/check_pr_target.py +81 -0
  11. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/job_environment_prefix.py +4 -2
  12. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/name_capitalized.py +4 -2
  13. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/name_exists.py +4 -2
  14. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/pinned_job_runner.py +4 -2
  15. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/run_actionlint.py +2 -2
  16. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/step_approved.py +4 -2
  17. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/step_pinned.py +4 -2
  18. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/underscore_outputs.py +4 -2
  19. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/utils.py +2 -2
  20. bitwarden_workflow_linter-0.7.0/tests/rules/test_check_pr_target.py +276 -0
  21. bitwarden_workflow_linter-0.5.6/settings.yaml +0 -11
  22. bitwarden_workflow_linter-0.5.6/src/bitwarden_workflow_linter/default_settings.yaml +0 -11
  23. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.editorconfig +0 -0
  24. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.gitattributes +0 -0
  25. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/CODEOWNERS +0 -0
  26. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  27. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  28. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/renovate.json +0 -0
  29. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/workflows/_version_type.yml +0 -0
  30. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/workflows/cd.yml +0 -0
  31. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/workflows/ci.yml +0 -0
  32. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/workflows/scan.yml +0 -0
  33. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.github/workflows/update_actions.yml +0 -0
  34. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.gitignore +0 -0
  35. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.husky/pre-commit +0 -0
  36. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/.python-version +0 -0
  37. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/CONTRIBUTING.md +0 -0
  38. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/LICENSE.txt +0 -0
  39. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/Pipfile +0 -0
  40. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/Pipfile.lock +0 -0
  41. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/SECURITY.md +0 -0
  42. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/Taskfile.yml +0 -0
  43. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/package-lock.json +0 -0
  44. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/package.json +0 -0
  45. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/pylintrc +0 -0
  46. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/pyproject.toml +0 -0
  47. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/pyproject.toml.tpl +0 -0
  48. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/__init__.py +0 -0
  49. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/actions.py +0 -0
  50. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/cli.py +0 -0
  51. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/lint.py +0 -0
  52. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/models/__init__.py +0 -0
  53. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/models/step.py +0 -0
  54. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/models/workflow.py +0 -0
  55. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rule.py +0 -0
  56. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/src/bitwarden_workflow_linter/rules/__init__.py +0 -0
  57. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/__init__.py +0 -0
  58. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/conftest.py +0 -0
  59. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/fixtures/test-alt.yml +0 -0
  60. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/fixtures/test-min-incorrect.yaml +0 -0
  61. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/fixtures/test-min.yaml +0 -0
  62. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/fixtures/test-outputs-incorrect.yml +0 -0
  63. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/fixtures/test.yml +0 -0
  64. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/fixtures/test_a.yaml +0 -0
  65. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/fixtures/test_workflow.yaml +0 -0
  66. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/fixtures/test_workflow_incorrect.yaml +0 -0
  67. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/__init__.py +0 -0
  68. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/test_job_environment_prefix.py +0 -0
  69. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/test_name_capitalized.py +0 -0
  70. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/test_name_exists.py +0 -0
  71. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/test_pinned_job_runner.py +0 -0
  72. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/test_run_actionlint.py +0 -0
  73. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/test_step_approved.py +0 -0
  74. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/test_step_pinned.py +0 -0
  75. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/rules/test_underscore_output.py +0 -0
  76. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/test_job.py +0 -0
  77. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/test_lint.py +0 -0
  78. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/test_load.py +0 -0
  79. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/test_rule.py +0 -0
  80. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/test_step.py +0 -0
  81. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/test_utils.py +0 -0
  82. {bitwarden_workflow_linter-0.5.6 → bitwarden_workflow_linter-0.7.0}/tests/test_workflow.py +0 -0
@@ -9,7 +9,7 @@ jobs:
9
9
  uses: bitwarden/gh-actions/.github/workflows/_enforce-labels.yml@main
10
10
 
11
11
  enforce-version-label:
12
- if: "!(contains(github.event.pull_request.labels.*.name, 'version:major') || contains(github.event.pull_request.labels.*.name, 'version:minor') || contains(github.event.pull_request.labels.*.name, 'version:patch')) || contains(github.event.pull_request.labels.*.name, 'version:skip')"
12
+ if: "!(contains(github.event.pull_request.labels.*.name, 'version:major') || contains(github.event.pull_request.labels.*.name, 'version:minor') || contains(github.event.pull_request.labels.*.name, 'version:patch') || contains(github.event.pull_request.labels.*.name, 'version:skip'))"
13
13
  name: Enforce version label
14
14
  runs-on: ubuntu-22.04
15
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bitwarden_workflow_linter
3
- Version: 0.5.6
3
+ Version: 0.7.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
@@ -60,12 +60,20 @@ the below and create a `settings.yaml` in the directory that `bwwl` will be runn
60
60
 
61
61
  ```yaml
62
62
  enabled_rules:
63
- - bitwarden_workflow_linter.rules.name_exists.RuleNameExists
64
- - bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
65
- - bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
66
- - bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
67
- - bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
68
- - bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs
63
+ - id: bitwarden_workflow_linter.rules.name_exists.RuleNameExists
64
+ level: error
65
+ - id: bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
66
+ level: error
67
+ - id: bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
68
+ level: error
69
+ - id: bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
70
+ level: error
71
+ - id: bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
72
+ level: error
73
+ - id: bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs
74
+ level: warning
75
+ - id: bitwarden_workflow_linter.rules.run_actionlint.RunActionlint
76
+ level: warning
69
77
 
70
78
  approved_actions_path: default_actions.json
71
79
  ```
@@ -151,9 +159,9 @@ from ..utils import LintLevels, Settings
151
159
 
152
160
 
153
161
  class RuleJobNameExists(Rule):
154
- def __init__(self, settings: Settings = None) -> None:
162
+ def __init__(self, settings: Settings = None, lint_level: Optional[LintLevels] = LintLevels.ERROR) -> None:
155
163
  self.message = "name must exist"
156
- self.on_fail: LintLevels = LintLevels.ERROR
164
+ self.on_fail: LintLevels = lint_level
157
165
  self.compatibility: List[Union[Workflow, Job, Step]] = [Job]
158
166
  self.settings: Settings = settings
159
167
 
@@ -34,12 +34,20 @@ the below and create a `settings.yaml` in the directory that `bwwl` will be runn
34
34
 
35
35
  ```yaml
36
36
  enabled_rules:
37
- - bitwarden_workflow_linter.rules.name_exists.RuleNameExists
38
- - bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
39
- - bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
40
- - bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
41
- - bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
42
- - bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs
37
+ - id: bitwarden_workflow_linter.rules.name_exists.RuleNameExists
38
+ level: error
39
+ - id: bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
40
+ level: error
41
+ - id: bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
42
+ level: error
43
+ - id: bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
44
+ level: error
45
+ - id: bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
46
+ level: error
47
+ - id: bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs
48
+ level: warning
49
+ - id: bitwarden_workflow_linter.rules.run_actionlint.RunActionlint
50
+ level: warning
43
51
 
44
52
  approved_actions_path: default_actions.json
45
53
  ```
@@ -125,9 +133,9 @@ from ..utils import LintLevels, Settings
125
133
 
126
134
 
127
135
  class RuleJobNameExists(Rule):
128
- def __init__(self, settings: Settings = None) -> None:
136
+ def __init__(self, settings: Settings = None, lint_level: Optional[LintLevels] = LintLevels.ERROR) -> None:
129
137
  self.message = "name must exist"
130
- self.on_fail: LintLevels = LintLevels.ERROR
138
+ self.on_fail: LintLevels = lint_level
131
139
  self.compatibility: List[Union[Workflow, Job, Step]] = [Job]
132
140
  self.settings: Settings = settings
133
141
 
@@ -0,0 +1,21 @@
1
+ enabled_rules:
2
+ - id: bitwarden_workflow_linter.rules.name_exists.RuleNameExists
3
+ level: error
4
+ - id: bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
5
+ level: error
6
+ - id: bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
7
+ level: error
8
+ - id: bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
9
+ level: error
10
+ # - id: bitwarden_workflow_linter.rules.step_approved.RuleStepUsesApproved
11
+ # level: error
12
+ - id: bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
13
+ level: error
14
+ - id: bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs
15
+ level: warning
16
+ - id: bitwarden_workflow_linter.rules.run_actionlint.RunActionlint
17
+ level: warning
18
+ - id: bitwarden_workflow_linter.rules.check_pr_target.RuleCheckPrTarget
19
+ level: warning
20
+
21
+ approved_actions_path: default_actions.json
@@ -1,3 +1,3 @@
1
1
  """Metadata for Workflow Linter."""
2
2
 
3
- __version__ = "0.5.6"
3
+ __version__ = "0.7.0"
@@ -121,8 +121,8 @@
121
121
  },
122
122
  "actions/upload-artifact": {
123
123
  "name": "actions/upload-artifact",
124
- "sha": "b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882",
125
- "version": "v4.4.3"
124
+ "sha": "65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08",
125
+ "version": "v4.6.0"
126
126
  },
127
127
  "actions/upload-pages-artifact": {
128
128
  "name": "actions/upload-pages-artifact",
@@ -221,8 +221,8 @@
221
221
  },
222
222
  "docker/build-push-action": {
223
223
  "name": "docker/build-push-action",
224
- "sha": "48aba3b46d1b1fec4febb7c5d0c644b249a11355",
225
- "version": "v6.10.0"
224
+ "sha": "67a2d409c0a876cbe6b11854e3e25193efe4e62d",
225
+ "version": "v6.12.0"
226
226
  },
227
227
  "docker/login-action": {
228
228
  "name": "docker/login-action",
@@ -274,6 +274,11 @@
274
274
  "sha": "cc4fc85e6b35bafd578d5ffbc76a5518407e1af0",
275
275
  "version": "v4.2.1"
276
276
  },
277
+ "gradle/wrapper-validation-action": {
278
+ "name": "gradle/wrapper-validation-action",
279
+ "sha": "f9c9c575b8b21b6485636a91ffecd10e558c62f6",
280
+ "version": "v3.5.0"
281
+ },
277
282
  "hashicorp/setup-packer": {
278
283
  "name": "hashicorp/setup-packer",
279
284
  "sha": "1aa358be5cf73883762b302a3a03abd66e75b232",
@@ -0,0 +1,21 @@
1
+ enabled_rules:
2
+ - id: bitwarden_workflow_linter.rules.name_exists.RuleNameExists
3
+ level: error
4
+ - id: bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
5
+ level: error
6
+ - id: bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
7
+ level: error
8
+ - id: bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
9
+ level: error
10
+ # - id: bitwarden_workflow_linter.rules.step_approved.RuleStepUsesApproved
11
+ # level: error
12
+ - id: bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
13
+ level: error
14
+ - id: bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs
15
+ level: warning
16
+ - id: bitwarden_workflow_linter.rules.run_actionlint.RunActionlint
17
+ level: warning
18
+ - id: bitwarden_workflow_linter.rules.check_pr_target.RuleCheckPrTarget
19
+ level: warning
20
+
21
+ approved_actions_path: default_actions.json
@@ -11,7 +11,7 @@ from .models.job import Job
11
11
  from .models.step import Step
12
12
  from .models.workflow import Workflow
13
13
  from .rule import Rule
14
- from .utils import Settings
14
+ from .utils import Settings, LintLevels
15
15
 
16
16
  yaml = YAML()
17
17
 
@@ -127,13 +127,14 @@ class Rules:
127
127
  """
128
128
  # [TODO]: data resiliency
129
129
  for rule in settings.enabled_rules:
130
- module_name = rule.split(".")
130
+ rule_id = rule["id"]
131
+ module_name = rule_id.split(".")
131
132
  module_name = ".".join(module_name[:-1])
132
- rule_name = rule.split(".")[-1]
133
+ rule_name = rule_id.split(".")[-1]
133
134
 
134
135
  try:
135
136
  rule_class = getattr(importlib.import_module(module_name), rule_name)
136
- rule_inst = rule_class(settings=settings)
137
+ rule_inst = rule_class(settings=settings, lint_level=lint_level(rule["level"]))
137
138
 
138
139
  if Workflow in rule_inst.compatibility:
139
140
  self.workflow.append(rule_inst)
@@ -157,3 +158,21 @@ class Rules:
157
158
  for rule in self.step:
158
159
  print(f" - {type(rule).__name__}")
159
160
  print("========================\n")
161
+
162
+
163
+ def lint_level(level: str) -> LintLevels:
164
+ """Convert a string to a LintLevels enum.
165
+
166
+ Args:
167
+ level:
168
+ The string representation of the LintLevels enum
169
+
170
+ Returns:
171
+ The LintLevels enum value
172
+ """
173
+ if level == "error":
174
+ return LintLevels.ERROR
175
+ elif level == "warning":
176
+ return LintLevels.WARNING
177
+ else:
178
+ return LintLevels.NONE
@@ -23,6 +23,7 @@ class Job:
23
23
  key: Optional[str] = None
24
24
  name: Optional[str] = None
25
25
  env: Optional[CommentedMap] = None
26
+ needs: Optional[List[str]] = None
26
27
  steps: Optional[List[Step]] = None
27
28
  uses: Optional[str] = None
28
29
  uses_path: Optional[str] = None
@@ -32,6 +33,13 @@ class Job:
32
33
  )
33
34
  outputs: Optional[CommentedMap] = None
34
35
 
36
+ @classmethod
37
+ def parse_needs(cls: Self, value):
38
+ """Parser to make all needs values lists that can be searched by linter."""
39
+ if isinstance(value, str):
40
+ return [value]
41
+ return value
42
+
35
43
  @classmethod
36
44
  def init(cls: Self, key: str, data: CommentedMap) -> Self:
37
45
  """Custom dataclass constructor to map job data to a Job."""
@@ -40,6 +48,7 @@ class Job:
40
48
  "name": data["name"] if "name" in data else None,
41
49
  "runs-on": data["runs-on"] if "runs-on" in data else None,
42
50
  "env": data["env"] if "env" in data else None,
51
+ "needs": Job.parse_needs(data["needs"]) if "needs" in data else None,
43
52
  "outputs": data["outputs"] if "outputs" in data else None,
44
53
  }
45
54
 
@@ -0,0 +1,81 @@
1
+ """A Rule to enforce check-run is run when workflow uses pull_request_target."""
2
+
3
+ from typing import Optional, Tuple
4
+
5
+ from ..models.workflow import Workflow
6
+ from ..rule import Rule
7
+ from ..utils import LintLevels, Settings
8
+
9
+
10
+ class RuleCheckPrTarget(Rule):
11
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.NONE) -> None:
12
+ """
13
+ To ensure pull_request_target is safe to use, the check-run step is added
14
+ to all jobs as a dependency.
15
+
16
+ Once a branch is pushed to Github, it already opens up a vulnerability
17
+ even if the check-run scan fails to detect this.
18
+
19
+ In order to prevent a vulnerable branch from being used for an attack
20
+ prior to being caught through vetting, all pull_request_target workflows
21
+ should only be run by users with appropriate permissions.
22
+
23
+ This greatly reduces the risk as community contributors can't use a fork to run a compromised workflow that uses pull_request_target.
24
+ """
25
+ self.message = "A check-run job must be included as a direct job dependency when pull_request_target is used and the workflow may only apply to runs on the main branch"
26
+ self.on_fail = lint_level
27
+ self.compatibility = [Workflow]
28
+ self.settings = settings
29
+
30
+ def targets_main_branch(self, obj:Workflow) -> bool:
31
+ if obj.on["pull_request_target"].get("branches"):
32
+ branches_list = obj.on["pull_request_target"].get("branches")
33
+ if isinstance(branches_list, str):
34
+ branches_list = [branches_list]
35
+ if any(branch != 'main' for branch in branches_list):
36
+ return False
37
+ else:
38
+ return False
39
+ return True
40
+
41
+ def has_check_run(self, obj: Workflow) -> Tuple[bool, str]:
42
+ for name, job in obj.jobs.items():
43
+ if job.uses == "bitwarden/gh-actions/.github/workflows/check-run.yml@main":
44
+ return True, name
45
+ return False, ""
46
+
47
+ def check_run_required(self, obj:Workflow, check_job:str) -> list:
48
+ missing_jobs = []
49
+ for job in list(obj.jobs.keys()):
50
+ if job is not check_job:
51
+ job_list = obj.jobs[job].needs
52
+ if job_list:
53
+ if check_job not in job_list:
54
+ missing_jobs.append(job)
55
+ else:
56
+ missing_jobs.append(job)
57
+ return missing_jobs
58
+
59
+ def fn(self, obj: Workflow) -> Tuple[bool, str]:
60
+ Errors = []
61
+ if obj.on.get("pull_request_target"):
62
+ result, check_job = self.has_check_run(obj)
63
+ main_branch_only = self.targets_main_branch(obj)
64
+ if not main_branch_only:
65
+ Errors.append("Workflows using pull_request_target can only target the main branch")
66
+ if result:
67
+ missing_jobs = self.check_run_required(obj, check_job)
68
+ if missing_jobs:
69
+ job_list = ', '.join(missing_jobs)
70
+ message = f"Check-run is missing from the following jobs in the workflow: {job_list}"
71
+ Errors.append(message)
72
+ else:
73
+ message = f"A check-run job must be included as a direct job dependency when pull_request_target is used"
74
+ Errors.append(message)
75
+ if Errors:
76
+ self.message = "\n".join(Errors)
77
+ return False, f"{self.message}"
78
+ else:
79
+ return True, ""
80
+ else:
81
+ return True, ""
@@ -22,16 +22,18 @@ class RuleJobEnvironmentPrefix(Rule):
22
22
  visible when debugging a shell Step.
23
23
  """
24
24
 
25
- def __init__(self, settings: Optional[Settings] = None) -> None:
25
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.NONE) -> None:
26
26
  """RuleJobEnvironmentPrefix constructor to override the Rule class.
27
27
 
28
28
  Args:
29
29
  settings:
30
30
  A Settings object that contains any default, overridden, or custom settings
31
31
  required anywhere in the application.
32
+ lint_level:
33
+ The LintLevels enum value to determine the severity of the rule.
32
34
  """
33
35
  self.message = "Job environment vars should start with an underscore:"
34
- self.on_fail = LintLevels.ERROR
36
+ self.on_fail = lint_level
35
37
  self.compatibility = [Job]
36
38
  self.settings = settings
37
39
 
@@ -16,16 +16,18 @@ class RuleNameCapitalized(Rule):
16
16
  A simple standard to help keep uniformity in naming.
17
17
  """
18
18
 
19
- def __init__(self, settings: Optional[Settings] = None) -> None:
19
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.NONE) -> None:
20
20
  """Constructor for RuleNameCapitalized to override the Rule class.
21
21
 
22
22
  Args:
23
23
  settings:
24
24
  A Settings object that contains any default, overridden, or custom settings
25
25
  required anywhere in the application.
26
+ lint_level:
27
+ The LintLevels enum value to determine the severity of the rule.
26
28
  """
27
29
  self.message = "name must capitalized"
28
- self.on_fail = LintLevels.ERROR
30
+ self.on_fail = lint_level
29
31
  self.settings = settings
30
32
 
31
33
  def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]:
@@ -19,16 +19,18 @@ class RuleNameExists(Rule):
19
19
  It also helps with uniformity of runs.
20
20
  """
21
21
 
22
- def __init__(self, settings: Optional[Settings] = None) -> None:
22
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.NONE) -> None:
23
23
  """Constructor for RuleNameCapitalized to override Rule class.
24
24
 
25
25
  Args:
26
26
  settings:
27
27
  A Settings object that contains any default, overridden, or custom settings
28
28
  required anywhere in the application.
29
+ lint_level:
30
+ The LintLevels enum value to determine the severity of the rule.
29
31
  """
30
32
  self.message = "name must exist"
31
- self.on_fail = LintLevels.ERROR
33
+ self.on_fail = lint_level
32
34
  self.settings = settings
33
35
 
34
36
  def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]:
@@ -15,16 +15,18 @@ class RuleJobRunnerVersionPinned(Rule):
15
15
  breaking the majority of our pipelines, we pin the versions.
16
16
  """
17
17
 
18
- def __init__(self, settings: Optional[Settings] = None) -> None:
18
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.NONE) -> None:
19
19
  """Constructor for RuleJobRunnerVersionPinned to override Rule class.
20
20
 
21
21
  Args:
22
22
  settings:
23
23
  A Settings object that contains any default, overridden, or custom settings
24
24
  required anywhere in the application.
25
+ lint_level:
26
+ The LintLevels enum value to determine the severity of the rule.
25
27
  """
26
28
  self.message = "Workflow runner must be pinned"
27
- self.on_fail = LintLevels.ERROR
29
+ self.on_fail = lint_level
28
30
  self.compatibility = [Job]
29
31
  self.settings = settings
30
32
 
@@ -71,9 +71,9 @@ please check your package installer or manually install it",
71
71
  class RunActionlint(Rule):
72
72
  """Rule to run actionlint as part of workflow linter V2."""
73
73
 
74
- def __init__(self, settings: Optional[Settings] = None) -> None:
74
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.NONE) -> None:
75
75
  self.message = "Actionlint must pass without errors"
76
- self.on_fail = LintLevels.WARNING
76
+ self.on_fail = lint_level
77
77
  self.compatibility = [Workflow]
78
78
  self.settings = settings
79
79
 
@@ -15,15 +15,17 @@ class RuleStepUsesApproved(Rule):
15
15
  check against.
16
16
  """
17
17
 
18
- def __init__(self, settings: Optional[Settings] = None) -> None:
18
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.NONE) -> None:
19
19
  """Constructor for RuleStepUsesApproved to override Rule class.
20
20
 
21
21
  Args:
22
22
  settings:
23
23
  A Settings object that contains any default, overridden, or custom settings
24
24
  required anywhere in the application.
25
+ lint_level:
26
+ The LintLevels enum value to determine the severity of the rule.
25
27
  """
26
- self.on_fail = LintLevels.ERROR
28
+ self.on_fail = lint_level
27
29
  self.compatibility = [Step]
28
30
  self.settings = settings
29
31
 
@@ -23,15 +23,17 @@ class RuleStepUsesPinned(Rule):
23
23
  updated.
24
24
  """
25
25
 
26
- def __init__(self, settings: Optional[Settings] = None) -> None:
26
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.ERROR) -> None:
27
27
  """Constructor for RuleStepUsesPinned to override base Rule.
28
28
 
29
29
  Args:
30
30
  settings:
31
31
  A Settings object that contains any default, overridden, or custom settings
32
32
  required anywhere in the application.
33
+ lint_level:
34
+ The LintLevels enum value to determine the severity of the rule.
33
35
  """
34
- self.on_fail = LintLevels.ERROR
36
+ self.on_fail = lint_level
35
37
  self.compatibility = [Step]
36
38
  self.settings = settings
37
39
 
@@ -17,16 +17,18 @@ class RuleUnderscoreOutputs(Rule):
17
17
  A simple standard to ensure uniformity in naming.
18
18
  """
19
19
 
20
- def __init__(self, settings: Optional[Settings] = None) -> None:
20
+ def __init__(self, settings: Optional[Settings] = None, lint_level: Optional[LintLevels] = LintLevels.ERROR) -> None:
21
21
  """Constructor for RuleUnderscoreOutputs to override the Rule class.
22
22
 
23
23
  Args:
24
24
  settings:
25
25
  A Settings object that contains any default, overridden, or custom settings
26
26
  required anywhere in the application.
27
+ lint_level:
28
+ The LintLevels enum value to determine the severity of the rule.
27
29
  """
28
30
  self.message = "outputs with more than one word should use an underscore"
29
- self.on_fail = LintLevels.WARNING
31
+ self.on_fail = lint_level
30
32
  self.compatibility = [Workflow, Job, Step]
31
33
  self.settings = settings
32
34
 
@@ -111,12 +111,12 @@ SettingsFromFactory = TypeVar("SettingsFromFactory", bound="Settings")
111
111
  class Settings:
112
112
  """Class that contains configuration-as-code for any portion of the app."""
113
113
 
114
- enabled_rules: list[str]
114
+ enabled_rules: list[dict[str, str]]
115
115
  approved_actions: dict[str, Action]
116
116
 
117
117
  def __init__(
118
118
  self,
119
- enabled_rules: Optional[list[str]] = None,
119
+ enabled_rules: Optional[list[dict[str, str]]] = None,
120
120
  approved_actions: Optional[dict[str, dict[str, str]]] = None,
121
121
  ) -> None:
122
122
  """Settings object that can be overridden in settings.py.
@@ -0,0 +1,276 @@
1
+ """Test src/bitwarden_workflow_linter/rules/check_pr_target."""
2
+
3
+ import pytest
4
+
5
+ from ruamel.yaml import YAML
6
+
7
+ from src.bitwarden_workflow_linter.load import WorkflowBuilder
8
+ from src.bitwarden_workflow_linter.rules.check_pr_target import (
9
+ RuleCheckPrTarget,
10
+ )
11
+
12
+ yaml = YAML()
13
+ message = "A check-run job must be included as a direct job dependency when pull_request_target is used and the workflow may only apply to runs on the main branch"
14
+
15
+ @pytest.fixture(name="correct_workflow")
16
+ def fixture_correct_workflow():
17
+ workflow = """\
18
+ ---
19
+ on:
20
+ workflow_dispatch:
21
+ pull_request_target:
22
+ types: [opened, synchronize]
23
+ branches:
24
+ - 'main'
25
+
26
+ jobs:
27
+ check-run:
28
+ name: Check PR run
29
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
30
+
31
+ quality:
32
+ name: Quality scan
33
+ needs: check-run
34
+ steps:
35
+ - run: echo test
36
+
37
+ dependent-job:
38
+ name: Another Dependent Job
39
+ needs:
40
+ - quality
41
+ - check-run
42
+ steps:
43
+ - run: echo another dependent job
44
+ """
45
+ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False)
46
+
47
+ @pytest.fixture(name="no_check_workflow")
48
+ def fixture_no_check_workflow():
49
+ workflow = """\
50
+ ---
51
+ on:
52
+ workflow_dispatch:
53
+ pull_request_target:
54
+ types: [opened, synchronize]
55
+ branches:
56
+ - 'main'
57
+
58
+ jobs:
59
+ job-key:
60
+ runs-on: ubuntu-22.04
61
+ steps:
62
+ - run: echo test
63
+ """
64
+ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False)
65
+
66
+ @pytest.fixture(name="no_needs_workflow")
67
+ def fixture_no_needs_workflow():
68
+ workflow = """\
69
+ ---
70
+ on:
71
+ workflow_dispatch:
72
+ pull_request_target:
73
+ types: [opened, synchronize]
74
+ branches: main
75
+
76
+ jobs:
77
+ check-run:
78
+ name: Check PR run
79
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
80
+
81
+ job-key:
82
+ runs-on: ubuntu-22.04
83
+ steps:
84
+ - run: echo test
85
+
86
+ quality:
87
+ name: Quality scan
88
+ steps:
89
+ - run: echo test
90
+
91
+ """
92
+ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False)
93
+
94
+ @pytest.fixture(name="no_target_workflow")
95
+ def fixture_no_target_workflow():
96
+ workflow = """\
97
+ ---
98
+ on:
99
+ workflow_dispatch:
100
+
101
+ jobs:
102
+ job-key:
103
+ runs-on: ubuntu-22.04
104
+ steps:
105
+ - run: echo test
106
+ """
107
+ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False)
108
+
109
+ @pytest.fixture(name="dependent_missing_check_workflow")
110
+ def dependent_missing_check_workflow():
111
+ workflow = """\
112
+ ---
113
+ on:
114
+ workflow_dispatch:
115
+ pull_request_target:
116
+ types: [opened, synchronize]
117
+ branches:
118
+ - 'main'
119
+
120
+ jobs:
121
+ check-run:
122
+ name: Check PR run
123
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
124
+
125
+ quality:
126
+ name: Quality scan
127
+ needs: check-run
128
+ steps:
129
+ - run: echo test
130
+
131
+ dependent-job:
132
+ name: Another Dependent Job
133
+ needs:
134
+ - quality
135
+ steps:
136
+ - run: echo another dependent job
137
+ """
138
+ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False)
139
+
140
+ @pytest.fixture(name="no_branches_workflow")
141
+ def fixture_no_branches_workflow():
142
+ workflow = """\
143
+ ---
144
+ on:
145
+ workflow_dispatch:
146
+ pull_request_target:
147
+ types: [opened, synchronize]
148
+
149
+ jobs:
150
+ check-run:
151
+ name: Check PR run
152
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
153
+
154
+ quality:
155
+ name: Quality scan
156
+ needs: check-run
157
+ steps:
158
+ - run: echo test
159
+
160
+ dependent-job:
161
+ name: Another Dependent Job
162
+ needs:
163
+ - quality
164
+ - check-run
165
+ steps:
166
+ - run: echo another dependent job
167
+ """
168
+ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False)
169
+
170
+ @pytest.fixture(name="bad_branches_workflow")
171
+ def fixture_bad_branches_workflow():
172
+ workflow = """\
173
+ ---
174
+ on:
175
+ workflow_dispatch:
176
+ pull_request_target:
177
+ types: [opened, synchronize]
178
+ branches:
179
+ - 'main'
180
+ - 'not main'
181
+
182
+ jobs:
183
+ check-run:
184
+ name: Check PR run
185
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
186
+
187
+ quality:
188
+ name: Quality scan
189
+ needs: check-run
190
+ steps:
191
+ - run: echo test
192
+
193
+ dependent-job:
194
+ name: Another Dependent Job
195
+ needs:
196
+ - quality
197
+ - check-run
198
+ steps:
199
+ - run: echo another dependent job
200
+ """
201
+ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False)
202
+
203
+ @pytest.fixture(name="two_failures_workflow")
204
+ def fixture_two_failures_workflow():
205
+ workflow = """\
206
+ ---
207
+ on:
208
+ workflow_dispatch:
209
+ pull_request_target:
210
+ types: [opened, synchronize]
211
+
212
+ jobs:
213
+ check-run:
214
+ name: Check PR run
215
+ uses: bitwarden/some-other-repo/.github/workflows/check-run.yml@main
216
+
217
+ quality:
218
+ name: Quality scan
219
+ needs: check-run
220
+ steps:
221
+ - run: echo test
222
+
223
+ dependent-job:
224
+ name: Another Dependent Job
225
+ needs:
226
+ - quality
227
+ - check-run
228
+ steps:
229
+ - run: echo another dependent job
230
+ """
231
+ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False)
232
+
233
+ @pytest.fixture(name="rule")
234
+ def fixture_rule():
235
+ return RuleCheckPrTarget()
236
+
237
+ def test_rule_on_correct_workflow(rule, correct_workflow):
238
+ result, message = rule.fn(correct_workflow)
239
+ assert result is True
240
+ assert message == ""
241
+
242
+
243
+ def test_rule_on_no_checkworkflow(rule, no_check_workflow):
244
+ result, message = rule.fn(no_check_workflow)
245
+ assert result is False
246
+ assert message == message
247
+
248
+ def test_rule_on_no_target_workflow(rule, no_target_workflow):
249
+ result, message = rule.fn(no_target_workflow)
250
+ assert result is True
251
+ assert message == ""
252
+
253
+ def test_rule_on_jobs_without_needs(rule, no_needs_workflow):
254
+ result, message = rule.fn(no_needs_workflow)
255
+ assert result is False
256
+ assert message == message, "check-run is missing from the following jobs in the workflow: quality"
257
+
258
+ def test_rule_on_dependencies_without_check(rule, dependent_missing_check_workflow):
259
+ result, message = rule.fn(dependent_missing_check_workflow)
260
+ assert result is False
261
+ assert message == message, "check-run is missing from the following jobs in the workflow: dependent-job"
262
+
263
+ def test_rule_on_no_branches_workflow(rule, no_branches_workflow):
264
+ result, message = rule.fn(no_branches_workflow)
265
+ assert result is False
266
+ assert message == "Workflows using pull_request_target can only target the main branch"
267
+
268
+ def test_rule_on_only_target_main(rule, bad_branches_workflow):
269
+ result, message = rule.fn(bad_branches_workflow)
270
+ assert result is False
271
+ assert message == "Workflows using pull_request_target can only target the main branch"
272
+
273
+ def test_rule_on_two_failures(rule, two_failures_workflow):
274
+ result, message = rule.fn(two_failures_workflow)
275
+ assert result is False
276
+ assert message == "Workflows using pull_request_target can only target the main branch\nA check-run job must be included as a direct job dependency when pull_request_target is used"
@@ -1,11 +0,0 @@
1
- enabled_rules:
2
- - bitwarden_workflow_linter.rules.name_exists.RuleNameExists
3
- - bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
4
- - bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
5
- - bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
6
- # - bitwarden_workflow_linter.rules.step_approved.RuleStepUsesApproved
7
- - bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
8
- - bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs
9
- - bitwarden_workflow_linter.rules.run_actionlint.RunActionlint
10
-
11
- approved_actions_path: default_actions.json
@@ -1,11 +0,0 @@
1
- enabled_rules:
2
- - bitwarden_workflow_linter.rules.name_exists.RuleNameExists
3
- - bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
4
- - bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
5
- - bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
6
- # - bitwarden_workflow_linter.rules.step_approved.RuleStepUsesApproved
7
- - bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
8
- - bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs
9
- - bitwarden_workflow_linter.rules.run_actionlint.RunActionlint
10
-
11
- approved_actions_path: default_actions.json