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.
- bitwarden_workflow_linter/__about__.py +3 -0
- bitwarden_workflow_linter/__init__.py +0 -0
- bitwarden_workflow_linter/actions.py +218 -0
- bitwarden_workflow_linter/cli.py +55 -0
- bitwarden_workflow_linter/default_actions.json +262 -0
- bitwarden_workflow_linter/default_settings.yaml +8 -0
- bitwarden_workflow_linter/lint.py +173 -0
- bitwarden_workflow_linter/load.py +146 -0
- bitwarden_workflow_linter/models/__init__.py +0 -0
- bitwarden_workflow_linter/models/job.py +56 -0
- bitwarden_workflow_linter/models/step.py +48 -0
- bitwarden_workflow_linter/models/workflow.py +45 -0
- bitwarden_workflow_linter/rule.py +101 -0
- bitwarden_workflow_linter/rules/__init__.py +0 -0
- bitwarden_workflow_linter/rules/job_environment_prefix.py +72 -0
- bitwarden_workflow_linter/rules/name_capitalized.py +56 -0
- bitwarden_workflow_linter/rules/name_exists.py +59 -0
- bitwarden_workflow_linter/rules/pinned_job_runner.py +52 -0
- bitwarden_workflow_linter/rules/step_approved.py +101 -0
- bitwarden_workflow_linter/rules/step_pinned.py +98 -0
- bitwarden_workflow_linter/utils.py +179 -0
- bitwarden_workflow_linter-0.0.3.dist-info/METADATA +182 -0
- bitwarden_workflow_linter-0.0.3.dist-info/RECORD +26 -0
- bitwarden_workflow_linter-0.0.3.dist-info/WHEEL +4 -0
- bitwarden_workflow_linter-0.0.3.dist-info/entry_points.txt +2 -0
- bitwarden_workflow_linter-0.0.3.dist-info/licenses/LICENSE.txt +674 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
"""A Rule to enforce all 'name' values start with a capital letter."""
|
2
|
+
|
3
|
+
from typing import Optional, Tuple, Union
|
4
|
+
|
5
|
+
from ..models.job import Job
|
6
|
+
from ..models.step import Step
|
7
|
+
from ..models.workflow import Workflow
|
8
|
+
from ..rule import Rule
|
9
|
+
from ..utils import LintLevels, Settings
|
10
|
+
|
11
|
+
|
12
|
+
class RuleNameCapitalized(Rule):
|
13
|
+
"""Rule to enforce all 'name' values start with a capital letter.
|
14
|
+
|
15
|
+
A simple standard to help keep uniformity in naming.
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, settings: Optional[Settings] = None) -> None:
|
19
|
+
"""Constructor for RuleNameCapitalized to override the Rule class.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
settings:
|
23
|
+
A Settings object that contains any default, overridden, or custom settings
|
24
|
+
required anywhere in the application.
|
25
|
+
"""
|
26
|
+
self.message = "name must capitalized"
|
27
|
+
self.on_fail = LintLevels.ERROR
|
28
|
+
self.settings = settings
|
29
|
+
|
30
|
+
def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]:
|
31
|
+
"""Enforces capitalization of the first letter of any name key.
|
32
|
+
|
33
|
+
Example:
|
34
|
+
---
|
35
|
+
name: Test Workflow
|
36
|
+
|
37
|
+
on:
|
38
|
+
workflow_dispatch:
|
39
|
+
|
40
|
+
jobs:
|
41
|
+
job-key:
|
42
|
+
name: Test
|
43
|
+
runs-on: ubuntu-latest
|
44
|
+
steps:
|
45
|
+
- name: Test
|
46
|
+
run: echo test
|
47
|
+
|
48
|
+
'Test Workflow', 'Test', and 'Test' all start with a capital letter.
|
49
|
+
|
50
|
+
See tests/rules/test_name_capitalized.py for examples of incorrectly
|
51
|
+
capitalized names. This Rule DOES NOT enforce that the name exists.
|
52
|
+
It only enforces capitalization IF it does.
|
53
|
+
"""
|
54
|
+
if obj.name:
|
55
|
+
return obj.name[0].isupper(), self.message
|
56
|
+
return True, "" # Force passing if obj.name doesn't exist
|
@@ -0,0 +1,59 @@
|
|
1
|
+
"""A Rule to enforce that a 'name' key exists."""
|
2
|
+
|
3
|
+
from typing import Optional, Tuple, Union
|
4
|
+
|
5
|
+
from ..models.workflow import Workflow
|
6
|
+
from ..models.job import Job
|
7
|
+
from ..models.step import Step
|
8
|
+
from ..rule import Rule
|
9
|
+
from ..utils import LintLevels, Settings
|
10
|
+
|
11
|
+
|
12
|
+
class RuleNameExists(Rule):
|
13
|
+
"""Rule to enforce a 'name' key exists for every object in GitHub Actions.
|
14
|
+
|
15
|
+
For pipeline run troubleshooting and debugging, it is helpful to have a
|
16
|
+
name to immediately identify a Workflow, Job, or Step while moving between
|
17
|
+
run and the code.
|
18
|
+
|
19
|
+
It also helps with uniformity of runs.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, settings: Optional[Settings] = None) -> None:
|
23
|
+
"""Constructor for RuleNameCapitalized to override Rule class.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
settings:
|
27
|
+
A Settings object that contains any default, overridden, or custom settings
|
28
|
+
required anywhere in the application.
|
29
|
+
"""
|
30
|
+
self.message = "name must exist"
|
31
|
+
self.on_fail = LintLevels.ERROR
|
32
|
+
self.settings = settings
|
33
|
+
|
34
|
+
def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]:
|
35
|
+
"""Enforces the existence of names.
|
36
|
+
|
37
|
+
Example:
|
38
|
+
---
|
39
|
+
name: Test Workflow
|
40
|
+
|
41
|
+
on:
|
42
|
+
workflow_dispatch:
|
43
|
+
|
44
|
+
jobs:
|
45
|
+
job-key:
|
46
|
+
name: Test
|
47
|
+
runs-on: ubuntu-latest
|
48
|
+
steps:
|
49
|
+
- name: Test
|
50
|
+
run: echo test
|
51
|
+
|
52
|
+
'Test Workflow', 'Test', and 'Test' all exist.
|
53
|
+
|
54
|
+
See tests/rules/test_name_exists.py for examples where a name does not
|
55
|
+
exist.
|
56
|
+
"""
|
57
|
+
if obj.name is not None:
|
58
|
+
return True, ""
|
59
|
+
return False, self.message
|
@@ -0,0 +1,52 @@
|
|
1
|
+
"""A Rule to enforce pinning runners to a specific OS version."""
|
2
|
+
|
3
|
+
from typing import Optional, Tuple
|
4
|
+
|
5
|
+
from ..models.job import Job
|
6
|
+
from ..rule import Rule
|
7
|
+
from ..utils import LintLevels, Settings
|
8
|
+
|
9
|
+
|
10
|
+
class RuleJobRunnerVersionPinned(Rule):
|
11
|
+
"""Rule to enforce pinned Runner OS versions.
|
12
|
+
|
13
|
+
Using `*-latest` versions will update automatically and has broken all of
|
14
|
+
our workflows in the past. To avoid this and prevent a single event from
|
15
|
+
breaking the majority of our pipelines, we pin the versions.
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, settings: Optional[Settings] = None) -> None:
|
19
|
+
"""Constructor for RuleJobRunnerVersionPinned to override Rule class.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
settings:
|
23
|
+
A Settings object that contains any default, overridden, or custom settings
|
24
|
+
required anywhere in the application.
|
25
|
+
"""
|
26
|
+
self.message = "Workflow runner must be pinned"
|
27
|
+
self.on_fail = LintLevels.ERROR
|
28
|
+
self.compatibility = [Job]
|
29
|
+
self.settings = settings
|
30
|
+
|
31
|
+
def fn(self, obj: Job) -> Tuple[bool, str]:
|
32
|
+
"""Enforces runners are pinned to a version
|
33
|
+
|
34
|
+
Example:
|
35
|
+
---
|
36
|
+
on:
|
37
|
+
workflow_dispatch:
|
38
|
+
|
39
|
+
jobs:
|
40
|
+
job-key:
|
41
|
+
runs-on: ubuntu-22.04
|
42
|
+
steps:
|
43
|
+
- run: echo test
|
44
|
+
|
45
|
+
call-workflow:
|
46
|
+
uses: bitwarden/server/.github/workflows/workflow-linter.yml@master
|
47
|
+
|
48
|
+
'runs-on' is pinned to '22.04' instead of 'latest'
|
49
|
+
"""
|
50
|
+
if obj.runs_on is not None and "latest" in obj.runs_on:
|
51
|
+
return False, self.message
|
52
|
+
return True, ""
|
@@ -0,0 +1,101 @@
|
|
1
|
+
"""A Rule to enforce the use of a list of pre-approved Actions."""
|
2
|
+
|
3
|
+
from typing import Optional, Tuple
|
4
|
+
|
5
|
+
from ..models.step import Step
|
6
|
+
from ..rule import Rule
|
7
|
+
from ..utils import LintLevels, Settings
|
8
|
+
|
9
|
+
|
10
|
+
class RuleStepUsesApproved(Rule):
|
11
|
+
"""Rule to enforce that all Actions have been pre-approved.
|
12
|
+
|
13
|
+
To limit the surface area of a supply chain attack in our pipelines, all Actions
|
14
|
+
are required to pass a security review and be added to the pre-approved list to
|
15
|
+
check against.
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, settings: Optional[Settings] = None) -> None:
|
19
|
+
"""Constructor for RuleStepUsesApproved to override Rule class.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
settings:
|
23
|
+
A Settings object that contains any default, overridden, or custom settings
|
24
|
+
required anywhere in the application.
|
25
|
+
"""
|
26
|
+
self.on_fail = LintLevels.WARNING
|
27
|
+
self.compatibility = [Step]
|
28
|
+
self.settings = settings
|
29
|
+
|
30
|
+
def skip(self, obj: Step) -> bool:
|
31
|
+
"""Skip this Rule on some Steps.
|
32
|
+
|
33
|
+
This Rule does not apply to a few types of Steps. These
|
34
|
+
Rules are skipped.
|
35
|
+
"""
|
36
|
+
## Force pass for any shell steps
|
37
|
+
if not obj.uses:
|
38
|
+
return True
|
39
|
+
|
40
|
+
## Force pass for any local actions
|
41
|
+
if "@" not in obj.uses:
|
42
|
+
return True
|
43
|
+
|
44
|
+
## Force pass for any bitwarden/gh-actions
|
45
|
+
if obj.uses.startswith("bitwarden/gh-actions"):
|
46
|
+
return True
|
47
|
+
|
48
|
+
return False
|
49
|
+
|
50
|
+
def fn(self, obj: Step) -> Tuple[bool, str]:
|
51
|
+
"""Enforces all externally used Actions are on the pre-approved list.
|
52
|
+
|
53
|
+
The pre-approved list allows tight auditing on what Actions are trusted
|
54
|
+
and allowed to be run in our environments. This helps mitigate risks
|
55
|
+
against supply chain attacks in our pipelines.
|
56
|
+
|
57
|
+
Example:
|
58
|
+
---
|
59
|
+
on:
|
60
|
+
workflow_dispatch:
|
61
|
+
|
62
|
+
jobs:
|
63
|
+
job-key:
|
64
|
+
runs-on: ubuntu-22.04
|
65
|
+
steps:
|
66
|
+
- name: Checkout Branch
|
67
|
+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
68
|
+
|
69
|
+
- name: Test Bitwarden Action
|
70
|
+
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
71
|
+
|
72
|
+
- name: Test Local Action
|
73
|
+
uses: ./actions/test-action
|
74
|
+
|
75
|
+
- name: Test Run Action
|
76
|
+
run: echo "test"
|
77
|
+
|
78
|
+
In this example, 'actions/checkout' must be on the pre-approved list
|
79
|
+
and the metadata must match in order to succeed. The other three
|
80
|
+
Steps will be skipped.
|
81
|
+
"""
|
82
|
+
if self.skip(obj):
|
83
|
+
return True, ""
|
84
|
+
|
85
|
+
# Actions in bitwarden/gh-actions are auto-approved
|
86
|
+
if obj.uses and not obj.uses_path in self.settings.approved_actions:
|
87
|
+
return False, (
|
88
|
+
f"New Action detected: {obj.uses_path}\nFor security purposes, "
|
89
|
+
"actions must be reviewed and be on the pre-approved list"
|
90
|
+
)
|
91
|
+
|
92
|
+
action = self.settings.approved_actions[obj.uses_path]
|
93
|
+
|
94
|
+
if obj.uses_version != action.version or obj.uses_ref != action.sha:
|
95
|
+
return False, (
|
96
|
+
"Action is out of date. Please update to:\n"
|
97
|
+
f" commit: {action.version}"
|
98
|
+
f" version: {action.sha}"
|
99
|
+
)
|
100
|
+
|
101
|
+
return True, ""
|
@@ -0,0 +1,98 @@
|
|
1
|
+
"""A Rule to enforce Actions are pinned correctly."""
|
2
|
+
|
3
|
+
from typing import Optional, Tuple
|
4
|
+
|
5
|
+
from ..models.step import Step
|
6
|
+
from ..rule import Rule
|
7
|
+
from ..utils import LintLevels, Settings
|
8
|
+
|
9
|
+
|
10
|
+
class RuleStepUsesPinned(Rule):
|
11
|
+
"""Rule to contain the enforcement logic for pinning Actions versions.
|
12
|
+
|
13
|
+
Definition of Internal Action:
|
14
|
+
An Action that exists in the `bitwarden/gh-actions` GitHub Repository.
|
15
|
+
|
16
|
+
For any external Action (any Action that does not fit the above definition of
|
17
|
+
an Internal Action), to mitigate the risks of supply chain attacks in our CI
|
18
|
+
pipelines, we pin any use of an Action to a specific hash that has been verified
|
19
|
+
and pre-approved after a security audit of the version of the Action.
|
20
|
+
|
21
|
+
All Internal Actions, should be pinned to 'main'. This prevents Renovate from
|
22
|
+
spamming a bunch of PRs across all of our repos when `bitwarden/gh-actions` is
|
23
|
+
updated.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, settings: Optional[Settings] = None) -> None:
|
27
|
+
"""Constructor for RuleStepUsesPinned to override base Rule.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
settings:
|
31
|
+
A Settings object that contains any default, overridden, or custom settings
|
32
|
+
required anywhere in the application.
|
33
|
+
"""
|
34
|
+
self.on_fail = LintLevels.ERROR
|
35
|
+
self.compatibility = [Step]
|
36
|
+
self.settings = settings
|
37
|
+
|
38
|
+
def skip(self, obj: Step) -> bool:
|
39
|
+
"""Skip this Rule on some Steps.
|
40
|
+
|
41
|
+
This Rule does not apply to a few types of Steps. These
|
42
|
+
Rules are skipped.
|
43
|
+
"""
|
44
|
+
if not obj.uses:
|
45
|
+
return True
|
46
|
+
|
47
|
+
## Force pass for any local actions
|
48
|
+
if "@" not in obj.uses:
|
49
|
+
return True
|
50
|
+
|
51
|
+
return False
|
52
|
+
|
53
|
+
def fn(self, obj: Step) -> Tuple[bool, str]:
|
54
|
+
"""Enforces all Actions to be pinned in a specific way.
|
55
|
+
|
56
|
+
Pinning external Action hashes prevents unknown updates that could
|
57
|
+
break the pipelines or be the entry point to a supply chain attack.
|
58
|
+
|
59
|
+
Pinning internal Actions to branches allow for less updates as work
|
60
|
+
is done on those repos. This is mainly to support our Action
|
61
|
+
monorepo architecture of our Actions.
|
62
|
+
|
63
|
+
Example:
|
64
|
+
- name: Checkout Branch
|
65
|
+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
66
|
+
|
67
|
+
- name: Test Bitwarden Action
|
68
|
+
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
69
|
+
|
70
|
+
- name: Test Local Action
|
71
|
+
uses: ./actions/test-action
|
72
|
+
|
73
|
+
- name: Test Run Action
|
74
|
+
run: echo "test"
|
75
|
+
|
76
|
+
In this example, 'actions/checkout' must be pinned to the full commit
|
77
|
+
of the tag while 'bitwarden/gh-actions/get-keyvault-secrets' must be
|
78
|
+
pinned to 'main'. The other two Steps will be skipped.
|
79
|
+
"""
|
80
|
+
if self.skip(obj):
|
81
|
+
return True, ""
|
82
|
+
|
83
|
+
path, ref = obj.uses.split("@")
|
84
|
+
|
85
|
+
if path.startswith("bitwarden/gh-actions"):
|
86
|
+
if ref == "main":
|
87
|
+
return True, ""
|
88
|
+
return False, "Please pin to main"
|
89
|
+
|
90
|
+
try:
|
91
|
+
int(ref, 16)
|
92
|
+
except ValueError:
|
93
|
+
return False, "Please pin the action to a commit sha"
|
94
|
+
|
95
|
+
if len(ref) != 40:
|
96
|
+
return False, "Please use the full commit sha to pin the action"
|
97
|
+
|
98
|
+
return True, ""
|
@@ -0,0 +1,179 @@
|
|
1
|
+
"""Module of a collection of random utilities."""
|
2
|
+
|
3
|
+
import importlib.resources
|
4
|
+
import json
|
5
|
+
import os
|
6
|
+
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from enum import Enum
|
9
|
+
from typing import Optional, Self, TypeVar
|
10
|
+
|
11
|
+
from ruamel.yaml import YAML
|
12
|
+
|
13
|
+
|
14
|
+
yaml = YAML()
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class Colors:
|
19
|
+
"""Class containing color codes for printing strings to output."""
|
20
|
+
|
21
|
+
black = "30m"
|
22
|
+
red = "31m"
|
23
|
+
green = "32m"
|
24
|
+
yellow = "33m"
|
25
|
+
blue = "34m"
|
26
|
+
magenta = "35m"
|
27
|
+
cyan = "36m"
|
28
|
+
white = "37m"
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class LintLevel:
|
33
|
+
"""Class to contain the numeric level and color of linting."""
|
34
|
+
|
35
|
+
code: int
|
36
|
+
color: Colors
|
37
|
+
|
38
|
+
|
39
|
+
class LintLevels(LintLevel, Enum):
|
40
|
+
"""Collection of the different types of LintLevels available."""
|
41
|
+
|
42
|
+
NONE = 0, Colors.white
|
43
|
+
WARNING = 1, Colors.yellow
|
44
|
+
ERROR = 2, Colors.red
|
45
|
+
|
46
|
+
|
47
|
+
class LintFinding:
|
48
|
+
"""Represents a problem detected by linting."""
|
49
|
+
|
50
|
+
def __init__(self, description: str, level: LintLevels) -> None:
|
51
|
+
self.description = description
|
52
|
+
self.level = level
|
53
|
+
|
54
|
+
def __str__(self) -> str:
|
55
|
+
"""String representation of the class.
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
String representation of itself.
|
59
|
+
"""
|
60
|
+
return (
|
61
|
+
f"\033[{self.level.color}{self.level.name.lower()}\033[0m "
|
62
|
+
f"{self.description}"
|
63
|
+
)
|
64
|
+
|
65
|
+
|
66
|
+
@dataclass
|
67
|
+
class Action:
|
68
|
+
"""Collection of the metadata associated with a GitHub Action."""
|
69
|
+
|
70
|
+
name: str
|
71
|
+
version: str = ""
|
72
|
+
sha: str = ""
|
73
|
+
|
74
|
+
def __eq__(self, other: Self) -> bool:
|
75
|
+
"""Override Action equality.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
other:
|
79
|
+
Another Action type object to compare
|
80
|
+
|
81
|
+
Return
|
82
|
+
The state of equality
|
83
|
+
"""
|
84
|
+
return (
|
85
|
+
self.name == other.name
|
86
|
+
and self.version == other.version
|
87
|
+
and self.sha == other.sha
|
88
|
+
)
|
89
|
+
|
90
|
+
def __ne__(self, other: Self) -> bool:
|
91
|
+
"""Override Action unequality.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
other:
|
95
|
+
Another Action type object to compare
|
96
|
+
|
97
|
+
Return
|
98
|
+
The negation of the state of equality
|
99
|
+
"""
|
100
|
+
return not self.__eq__(other)
|
101
|
+
|
102
|
+
|
103
|
+
class SettingsError(Exception):
|
104
|
+
"""Custom Exception to indicate an error with loading Settings."""
|
105
|
+
|
106
|
+
pass
|
107
|
+
|
108
|
+
|
109
|
+
SettingsFromFactory = TypeVar("SettingsFromFactory", bound="Settings")
|
110
|
+
|
111
|
+
|
112
|
+
class Settings:
|
113
|
+
"""Class that contains configuration-as-code for any portion of the app."""
|
114
|
+
|
115
|
+
enabled_rules: list[str]
|
116
|
+
approved_actions: dict[str, Action]
|
117
|
+
|
118
|
+
def __init__(
|
119
|
+
self,
|
120
|
+
enabled_rules: Optional[list[str]] = None,
|
121
|
+
approved_actions: Optional[dict[str, dict[str, str]]] = None,
|
122
|
+
) -> None:
|
123
|
+
"""Settings object that can be overridden in settings.py.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
enabled_rules:
|
127
|
+
All of the python modules that implement a Rule to be run against
|
128
|
+
the workflows. These must be available somewhere on the PYTHONPATH
|
129
|
+
approved_actions:
|
130
|
+
The colleciton of GitHub Actions that are pre-approved to be used
|
131
|
+
in any workflow (Required by src.rules.step_approved)
|
132
|
+
"""
|
133
|
+
if enabled_rules is None:
|
134
|
+
enabled_rules = []
|
135
|
+
|
136
|
+
if approved_actions is None:
|
137
|
+
approved_actions = {}
|
138
|
+
|
139
|
+
self.enabled_rules = enabled_rules
|
140
|
+
self.approved_actions = {
|
141
|
+
name: Action(**action) for name, action in approved_actions.items()
|
142
|
+
}
|
143
|
+
|
144
|
+
@staticmethod
|
145
|
+
def factory() -> SettingsFromFactory:
|
146
|
+
with (
|
147
|
+
importlib.resources.files("bitwarden_workflow_linter")
|
148
|
+
.joinpath("default_settings.yaml")
|
149
|
+
.open("r", encoding="utf-8") as file
|
150
|
+
):
|
151
|
+
settings = yaml.load(file)
|
152
|
+
|
153
|
+
settings_filename = "settings.yaml"
|
154
|
+
local_settings = None
|
155
|
+
|
156
|
+
if os.path.exists(settings_filename):
|
157
|
+
with open(settings_filename, encoding="utf8") as settings_file:
|
158
|
+
local_settings = yaml.load(settings_file)
|
159
|
+
|
160
|
+
if local_settings:
|
161
|
+
settings.update(local_settings)
|
162
|
+
|
163
|
+
if settings["approved_actions_path"] == "default_actions.json":
|
164
|
+
with (
|
165
|
+
importlib.resources.files("bitwarden_workflow_linter")
|
166
|
+
.joinpath("default_actions.json")
|
167
|
+
.open("r", encoding="utf-8") as file
|
168
|
+
):
|
169
|
+
settings["approved_actions"] = json.load(file)
|
170
|
+
else:
|
171
|
+
with open(
|
172
|
+
settings["approved_actions_path"], "r", encoding="utf8"
|
173
|
+
) as action_file:
|
174
|
+
settings["approved_actions"] = json.load(action_file)
|
175
|
+
|
176
|
+
return Settings(
|
177
|
+
enabled_rules=settings["enabled_rules"],
|
178
|
+
approved_actions=settings["approved_actions"],
|
179
|
+
)
|