bitwarden_workflow_linter 0.0.3__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.
@@ -0,0 +1,3 @@
1
+ """Metadata for Workflow Linter."""
2
+
3
+ __version__ = "0.0.3"
File without changes
@@ -0,0 +1,218 @@
1
+ """Module providing Actions subcommand to manage list of pre-approved Actions."""
2
+
3
+ import argparse
4
+ import json
5
+ import logging
6
+ import os
7
+ import urllib3 as urllib
8
+
9
+ from dataclasses import asdict
10
+ from typing import Optional, Union
11
+
12
+ from .utils import Colors, Settings, Action
13
+
14
+
15
+ class GitHubApiSchemaError(Exception):
16
+ """A generic Exception to catch redefinitions of GitHub Api Schema changes."""
17
+
18
+ pass
19
+
20
+
21
+ class ActionsCmd:
22
+ """Command to manage the pre-approved list of Actions
23
+
24
+ This class contains logic to manage the list of pre-approved actions
25
+ to include:
26
+ - updating the action data in the list
27
+ - adding a new pre-approved action to the list with the data from the
28
+ latest release
29
+
30
+ This class also includes supporting logic to interact with GitHub
31
+
32
+ """
33
+
34
+ def __init__(self, settings: Optional[Settings] = None) -> None:
35
+ """Initialize the ActionsCmd class.
36
+
37
+ Args:
38
+ settings:
39
+ A Settings object that contains any default, overridden, or custom settings
40
+ required anywhere in the application.
41
+ """
42
+ self.settings = settings
43
+
44
+ @staticmethod
45
+ def extend_parser(
46
+ subparsers: argparse._SubParsersAction,
47
+ ) -> argparse._SubParsersAction:
48
+ """Extends the CLI subparser with the options for ActionCmd.
49
+
50
+ Add 'actions add' and 'actions update' to the CLI as subcommands
51
+ along with the options and arguments for each.
52
+
53
+ Args:
54
+ subparsers:
55
+ The main argument parser to add subcommands and arguments to
56
+ """
57
+ parser_actions = subparsers.add_parser(
58
+ "actions", help="!!BETA!!\nAdd or Update Actions in the pre-approved list."
59
+ )
60
+ parser_actions.add_argument(
61
+ "-o", "--output", action="store", default="actions.json"
62
+ )
63
+ subparsers_actions = parser_actions.add_subparsers(
64
+ required=True, dest="actions_command"
65
+ )
66
+ subparsers_actions.add_parser("update", help="update action versions")
67
+ parser_actions_add = subparsers_actions.add_parser(
68
+ "add", help="add action to approved list"
69
+ )
70
+ parser_actions_add.add_argument("name", help="action name [git owner/repo]")
71
+
72
+ return subparsers
73
+
74
+ def get_github_api_response(
75
+ self, url: str, action_name: str
76
+ ) -> Union[urllib.response.BaseHTTPResponse, None]:
77
+ """Call GitHub API with error logging without throwing an exception."""
78
+
79
+ http = urllib.PoolManager()
80
+ headers = {"user-agent": "bw-linter"}
81
+
82
+ if os.getenv("GITHUB_TOKEN", None):
83
+ headers["Authorization"] = f"Token {os.environ['GITHUB_TOKEN']}"
84
+
85
+ response = http.request("GET", url, headers=headers)
86
+
87
+ if response.status == 403 and response.reason == "rate limit exceeded":
88
+ logging.error(
89
+ "Failed to call GitHub API for action: %s due to rate limit exceeded.",
90
+ action_name,
91
+ )
92
+ return None
93
+
94
+ if response.status == 401 and response.reason == "Unauthorized":
95
+ logging.error(
96
+ "Failed to call GitHub API for action: %s: %s.",
97
+ action_name,
98
+ response.data,
99
+ )
100
+ return None
101
+
102
+ return response
103
+
104
+ def exists(self, action: Action) -> bool:
105
+ """Takes an action id and checks if the action repository exists."""
106
+
107
+ url = f"https://api.github.com/repos/{action.name}"
108
+ response = self.get_github_api_response(url, action.name)
109
+
110
+ if response is None:
111
+ # Handle exceeding GitHub API limit by returning that the action exists
112
+ # without actually checking to prevent false errors on linter output. Only
113
+ # show it as an linter error.
114
+ return True
115
+
116
+ if response.status == 404:
117
+ return False
118
+
119
+ return True
120
+
121
+ def get_latest_version(self, action: Action) -> Action | None:
122
+ """Gets the latest version of the Action to compare against."""
123
+
124
+ try:
125
+ # Get tag from latest release
126
+ response = self.get_github_api_response(
127
+ f"https://api.github.com/repos/{action.name}/releases/latest",
128
+ action.name,
129
+ )
130
+ if not response:
131
+ return None
132
+
133
+ tag_name = json.loads(response.data)["tag_name"]
134
+
135
+ # Get the URL to the commit for the tag
136
+ response = self.get_github_api_response(
137
+ f"https://api.github.com/repos/{action.name}/git/ref/tags/{tag_name}",
138
+ action.name,
139
+ )
140
+ if not response:
141
+ return None
142
+
143
+ if json.loads(response.data)["object"]["type"] == "commit":
144
+ sha = json.loads(response.data)["object"]["sha"]
145
+ else:
146
+ url = json.loads(response.data)["object"]["url"]
147
+ # Follow the URL and get the commit sha for tags
148
+ response = self.get_github_api_response(url, action.name)
149
+ if not response:
150
+ return None
151
+
152
+ sha = json.loads(response.data)["object"]["sha"]
153
+ except KeyError as err:
154
+ raise GitHubApiSchemaError(
155
+ f"Error with the GitHub API Response Schema for either /releases or"
156
+ f"/tags: {err}"
157
+ ) from err
158
+
159
+ return Action(name=action.name, version=tag_name, sha=sha)
160
+
161
+ def save_actions(self, updated_actions: dict[str, Action], filename: str) -> None:
162
+ """Save Actions to disk.
163
+
164
+ This is used to track the list of approved actions.
165
+ """
166
+ with open(filename, "w", encoding="utf8") as action_file:
167
+ converted_updated_actions = {
168
+ name: asdict(action) for name, action in updated_actions.items()
169
+ }
170
+ action_file.write(
171
+ json.dumps(converted_updated_actions, indent=2, sort_keys=True)
172
+ )
173
+
174
+ def add(self, new_action_name: str, filename: str) -> int:
175
+ """Subcommand to add a new Action to the list of approved Actions.
176
+
177
+ 'actions add' will add an Action and all of its metadata and dump all
178
+ approved actions (including the new one) to either the default JSON file
179
+ or the one provided by '--output'
180
+ """
181
+ print("Actions: add")
182
+ updated_actions = self.settings.approved_actions
183
+ proposed_action = Action(name=new_action_name)
184
+
185
+ if self.exists(proposed_action):
186
+ latest = self.get_latest_version(proposed_action)
187
+ if latest:
188
+ updated_actions[latest.name] = latest
189
+
190
+ self.save_actions(updated_actions, filename)
191
+ return 0
192
+
193
+ def update(self, filename: str) -> int:
194
+ """Subcommand to update all of the versions of the approved actions.
195
+
196
+ 'actions update' will update all of the approved actions to the newest
197
+ version and dump all of the new data to either the default JSON file or
198
+ the one provided by '--output'
199
+ """
200
+ print("Actions: update")
201
+ updated_actions = {}
202
+ for action in self.settings.approved_actions.values():
203
+ if self.exists(action):
204
+ latest_release = self.get_latest_version(action)
205
+ if action != latest_release:
206
+ print(
207
+ (
208
+ f" - {action.name} \033[{Colors.yellow}changed\033[0m: "
209
+ f"({action.version}, {action.sha}) => ("
210
+ f"{latest_release.version}, {latest_release.sha})"
211
+ )
212
+ )
213
+ else:
214
+ print(f" - {action.name} \033[{Colors.green}ok\033[0m")
215
+ updated_actions[action.name] = latest_release
216
+
217
+ self.save_actions(updated_actions, filename)
218
+ return 0
@@ -0,0 +1,55 @@
1
+ """This is the entrypoint module for the workflow-linter CLI."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from typing import List, Optional
7
+
8
+ from .actions import ActionsCmd
9
+ from .lint import LinterCmd
10
+ from .utils import Settings
11
+
12
+
13
+ local_settings = Settings.factory()
14
+
15
+
16
+ def main(input_args: Optional[List[str]] = None) -> int:
17
+ """CLI utility to lint GitHub Action Workflows.
18
+
19
+ A CLI utility to enforce coding standards on GitHub Action workflows. The
20
+ utility also provides other subcommands to assist with other workflow
21
+ maintenance tasks; such as maintaining the list of approved GitHub Actions.
22
+ """
23
+ linter_cmd = LinterCmd(settings=local_settings)
24
+ actions_cmd = ActionsCmd(settings=local_settings)
25
+
26
+ # Read arguments from command line.
27
+ parser = argparse.ArgumentParser(prog="bwwl")
28
+ parser.add_argument("-v", "--verbose", action="store_true", default=False)
29
+ subparsers = parser.add_subparsers(required=True, dest="command")
30
+
31
+ subparsers = LinterCmd.extend_parser(subparsers)
32
+ subparsers = ActionsCmd.extend_parser(subparsers)
33
+
34
+ # Pull the arguments from the command line
35
+ input_args = sys.argv[1:]
36
+ if not input_args:
37
+ raise SystemExit(parser.print_help())
38
+
39
+ args = parser.parse_args(input_args)
40
+
41
+ if args.command == "lint":
42
+ return linter_cmd.run(args.files, args.strict)
43
+
44
+ if args.command == "actions":
45
+ print(f"{'-'*50}\n!!bwwl actions is in BETA!!\n{'-'*50}")
46
+ if args.actions_command == "add":
47
+ return actions_cmd.add(args.name, args.output)
48
+ if args.actions_command == "update":
49
+ return actions_cmd.update(args.output)
50
+
51
+ return -1
52
+
53
+
54
+ if __name__ == "__main__":
55
+ sys.exit(main())
@@ -0,0 +1,262 @@
1
+ {
2
+ "Asana/create-app-attachment-github-action": {
3
+ "name": "Asana/create-app-attachment-github-action",
4
+ "sha": "affc72d57bac733d864d4189ed69a9cbd61a9e4f",
5
+ "version": "v1.3"
6
+ },
7
+ "Azure/functions-action": {
8
+ "name": "Azure/functions-action",
9
+ "sha": "238dc3c45bb1b04e5d16ff9e75cddd1d86753bd6",
10
+ "version": "v1.5.1"
11
+ },
12
+ "Azure/get-keyvault-secrets": {
13
+ "name": "Azure/get-keyvault-secrets",
14
+ "sha": "b5c723b9ac7870c022b8c35befe620b7009b336f",
15
+ "version": "v1"
16
+ },
17
+ "Azure/login": {
18
+ "name": "Azure/login",
19
+ "sha": "de95379fe4dadc2defb305917eaa7e5dde727294",
20
+ "version": "v1.5.1"
21
+ },
22
+ "Swatinem/rust-cache": {
23
+ "name": "Swatinem/rust-cache",
24
+ "sha": "a95ba195448af2da9b00fb742d14ffaaf3c21f43",
25
+ "version": "v2.7.0"
26
+ },
27
+ "SwiftDocOrg/github-wiki-publish-action": {
28
+ "name": "SwiftDocOrg/github-wiki-publish-action",
29
+ "sha": "a87db85ed06e4431be29cfdcb22b9653881305d0",
30
+ "version": "1.0.0"
31
+ },
32
+ "SwiftDocOrg/swift-doc": {
33
+ "name": "SwiftDocOrg/swift-doc",
34
+ "sha": "f935ebfe524a0ff27bda07dadc3662e3e45b5125",
35
+ "version": "1.0.0-rc.1"
36
+ },
37
+ "act10ns/slack": {
38
+ "name": "act10ns/slack",
39
+ "sha": "ed1309ab9862e57e9e583e51c7889486b9a00b0f",
40
+ "version": "v2.0.0"
41
+ },
42
+ "actions/cache": {
43
+ "name": "actions/cache",
44
+ "sha": "704facf57e6136b1bc63b828d79edcd491f0ee84",
45
+ "version": "v3.3.2"
46
+ },
47
+ "actions/checkout": {
48
+ "name": "actions/checkout",
49
+ "sha": "b4ffde65f46336ab88eb53be808477a3936bae11",
50
+ "version": "v4.1.1"
51
+ },
52
+ "actions/delete-package-versions": {
53
+ "name": "actions/delete-package-versions",
54
+ "sha": "0d39a63126868f5eefaa47169615edd3c0f61e20",
55
+ "version": "v4.1.1"
56
+ },
57
+ "actions/download-artifact": {
58
+ "name": "actions/download-artifact",
59
+ "sha": "f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110",
60
+ "version": "v4.1.0"
61
+ },
62
+ "actions/github-script": {
63
+ "name": "actions/github-script",
64
+ "sha": "60a0d83039c74a4aee543508d2ffcb1c3799cdea",
65
+ "version": "v7.0.1"
66
+ },
67
+ "actions/labeler": {
68
+ "name": "actions/labeler",
69
+ "sha": "8558fd74291d67161a8a78ce36a881fa63b766a9",
70
+ "version": "v5.0.0"
71
+ },
72
+ "actions/setup-dotnet": {
73
+ "name": "actions/setup-dotnet",
74
+ "sha": "4d6c8fcf3c8f7a60068d26b594648e99df24cee3",
75
+ "version": "v4.0.0"
76
+ },
77
+ "actions/setup-java": {
78
+ "name": "actions/setup-java",
79
+ "sha": "387ac29b308b003ca37ba93a6cab5eb57c8f5f93",
80
+ "version": "v4.0.0"
81
+ },
82
+ "actions/setup-node": {
83
+ "name": "actions/setup-node",
84
+ "sha": "b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8",
85
+ "version": "v4.0.1"
86
+ },
87
+ "actions/setup-python": {
88
+ "name": "actions/setup-python",
89
+ "sha": "0a5c61591373683505ea898e09a3ea4f39ef2b9c",
90
+ "version": "v5.0.0"
91
+ },
92
+ "actions/stale": {
93
+ "name": "actions/stale",
94
+ "sha": "28ca1036281a5e5922ead5184a1bbf96e5fc984e",
95
+ "version": "v9.0.0"
96
+ },
97
+ "actions/upload-artifact": {
98
+ "name": "actions/upload-artifact",
99
+ "sha": "c7d193f32edcb7bfad88892161225aeda64e9392",
100
+ "version": "v4.0.0"
101
+ },
102
+ "android-actions/setup-android": {
103
+ "name": "android-actions/setup-android",
104
+ "sha": "07976c6290703d34c16d382cb36445f98bb43b1f",
105
+ "version": "v3.2.0"
106
+ },
107
+ "azure/webapps-deploy": {
108
+ "name": "azure/webapps-deploy",
109
+ "sha": "145a0687697df1d8a28909569f6e5d86213041f9",
110
+ "version": "v3.0.0"
111
+ },
112
+ "bitwarden/sm-action": {
113
+ "name": "bitwarden/sm-action",
114
+ "sha": "92d1d6a4f26a89a8191c83ab531a53544578f182",
115
+ "version": "v2.0.0"
116
+ },
117
+ "checkmarx/ast-github-action": {
118
+ "name": "checkmarx/ast-github-action",
119
+ "sha": "72d549beebd0bc5bbafa559f198161b6ce7c03df",
120
+ "version": "2.0.21"
121
+ },
122
+ "chrnorm/deployment-action": {
123
+ "name": "chrnorm/deployment-action",
124
+ "sha": "d42cde7132fcec920de534fffc3be83794335c00",
125
+ "version": "v2.0.5"
126
+ },
127
+ "chrnorm/deployment-status": {
128
+ "name": "chrnorm/deployment-status",
129
+ "sha": "2afb7d27101260f4a764219439564d954d10b5b0",
130
+ "version": "v2.0.1"
131
+ },
132
+ "chromaui/action": {
133
+ "name": "chromaui/action",
134
+ "sha": "80bf5911f28005ed208f15b7268843b79ca0e23a",
135
+ "version": "v1"
136
+ },
137
+ "cloudflare/pages-action": {
138
+ "name": "cloudflare/pages-action",
139
+ "sha": "f0a1cd58cd66095dee69bfa18fa5efd1dde93bca",
140
+ "version": "v1.5.0"
141
+ },
142
+ "convictional/trigger-workflow-and-wait": {
143
+ "name": "convictional/trigger-workflow-and-wait",
144
+ "sha": "f69fa9eedd3c62a599220f4d5745230e237904be",
145
+ "version": "v1.6.5"
146
+ },
147
+ "crazy-max/ghaction-import-gpg": {
148
+ "name": "crazy-max/ghaction-import-gpg",
149
+ "sha": "01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4",
150
+ "version": "v6.1.0"
151
+ },
152
+ "crowdin/github-action": {
153
+ "name": "crowdin/github-action",
154
+ "sha": "fdc55cdc519e86e32c22a07528d649277f1127f2",
155
+ "version": "v1.16.0"
156
+ },
157
+ "dawidd6/action-download-artifact": {
158
+ "name": "dawidd6/action-download-artifact",
159
+ "sha": "e7466d1a7587ed14867642c2ca74b5bcc1e19a2d",
160
+ "version": "v3.0.0"
161
+ },
162
+ "dawidd6/action-homebrew-bump-formula": {
163
+ "name": "dawidd6/action-homebrew-bump-formula",
164
+ "sha": "75ed025ff3ad1d617862838b342b06d613a0ddf3",
165
+ "version": "v3.10.1"
166
+ },
167
+ "digitalocean/action-doctl": {
168
+ "name": "digitalocean/action-doctl",
169
+ "sha": "e5cb5b0cde9789f79c5115c2c4d902f38a708804",
170
+ "version": "v2.5.0"
171
+ },
172
+ "docker/build-push-action": {
173
+ "name": "docker/build-push-action",
174
+ "sha": "4a13e500e55cf31b7a5d59a38ab2040ab0f42f56",
175
+ "version": "v5.1.0"
176
+ },
177
+ "docker/setup-buildx-action": {
178
+ "name": "docker/setup-buildx-action",
179
+ "sha": "f95db51fddba0c2d1ec667646a06c2ce06100226",
180
+ "version": "v3.0.0"
181
+ },
182
+ "docker/setup-qemu-action": {
183
+ "name": "docker/setup-qemu-action",
184
+ "sha": "68827325e0b33c7199eb31dd4e31fbe9023e06e3",
185
+ "version": "v3.0.0"
186
+ },
187
+ "dorny/test-reporter": {
188
+ "name": "dorny/test-reporter",
189
+ "sha": "afe6793191b75b608954023a46831a3fe10048d4",
190
+ "version": "v1.7.0"
191
+ },
192
+ "dtolnay/rust-toolchain": {
193
+ "name": "dtolnay/rust-toolchain",
194
+ "sha": "1482605bfc5719782e1267fd0c0cc350fe7646b8",
195
+ "version": "v1"
196
+ },
197
+ "futureware-tech/simulator-action": {
198
+ "name": "futureware-tech/simulator-action",
199
+ "sha": "bfa03d93ec9de6dacb0c5553bbf8da8afc6c2ee9",
200
+ "version": "v3"
201
+ },
202
+ "hashicorp/setup-packer": {
203
+ "name": "hashicorp/setup-packer",
204
+ "sha": "ecc5516821087666a672c0d280a0084ea6d9aafd",
205
+ "version": "v2.0.1"
206
+ },
207
+ "macauley/action-homebrew-bump-cask": {
208
+ "name": "macauley/action-homebrew-bump-cask",
209
+ "sha": "445c42390d790569d938f9068d01af39ca030feb",
210
+ "version": "v1.0.0"
211
+ },
212
+ "microsoft/setup-msbuild": {
213
+ "name": "microsoft/setup-msbuild",
214
+ "sha": "1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c",
215
+ "version": "v1.3.1"
216
+ },
217
+ "ncipollo/release-action": {
218
+ "name": "ncipollo/release-action",
219
+ "sha": "6c75be85e571768fa31b40abf38de58ba0397db5",
220
+ "version": "v1.13.0"
221
+ },
222
+ "peter-evans/close-issue": {
223
+ "name": "peter-evans/close-issue",
224
+ "sha": "276d7966e389d888f011539a86c8920025ea0626",
225
+ "version": "v3.0.1"
226
+ },
227
+ "ruby/setup-ruby": {
228
+ "name": "ruby/setup-ruby",
229
+ "sha": "360dc864d5da99d54fcb8e9148c14a84b90d3e88",
230
+ "version": "v1.165.1"
231
+ },
232
+ "samuelmeuli/action-snapcraft": {
233
+ "name": "samuelmeuli/action-snapcraft",
234
+ "sha": "d33c176a9b784876d966f80fb1b461808edc0641",
235
+ "version": "v2.1.1"
236
+ },
237
+ "snapcore/action-build": {
238
+ "name": "snapcore/action-build",
239
+ "sha": "2096990827aa966f773676c8a53793c723b6b40f",
240
+ "version": "v1.2.0"
241
+ },
242
+ "sonarsource/sonarcloud-github-action": {
243
+ "name": "sonarsource/sonarcloud-github-action",
244
+ "sha": "49e6cd3b187936a73b8280d59ffd9da69df63ec9",
245
+ "version": "v2.1.1"
246
+ },
247
+ "stackrox/kube-linter-action": {
248
+ "name": "stackrox/kube-linter-action",
249
+ "sha": "ca0d55b925470deb5b04b556e6c4276ea94d03c3",
250
+ "version": "v1.0.4"
251
+ },
252
+ "tj-actions/changed-files": {
253
+ "name": "tj-actions/changed-files",
254
+ "sha": "716b1e13042866565e00e85fd4ec490e186c4a2f",
255
+ "version": "v41.0.1"
256
+ },
257
+ "yogevbd/enforce-label-action": {
258
+ "name": "yogevbd/enforce-label-action",
259
+ "sha": "a3c219da6b8fa73f6ba62b68ff09c469b3a1c024",
260
+ "version": "2.2.2"
261
+ }
262
+ }
@@ -0,0 +1,8 @@
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_pinned.RuleStepUsesPinned
7
+
8
+ approved_actions_path: default_actions.json