-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Yarn v4 audit implementation #1184
Open
furnivall
wants to merge
35
commits into
master
Choose a base branch
from
yarn-v4
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
264b74f
add v4 audit implementation - testing
furnivall bb0b82d
fix some shell
furnivall 8550a98
fix some shell
furnivall 7313986
more fixes
furnivall cbc661f
more fixes
furnivall 08a0782
try block
furnivall 8d9b809
def on version
furnivall 22546df
commit
furnivall 0c776da
commit
furnivall 0fa9aed
switch quote type
furnivall fd8de07
man i'm dumb
furnivall 8a1909e
dumb error
furnivall dca51db
better catch block
furnivall 065cdd3
attempt to find this bloody exception
furnivall 0aab964
attempt to find this bloody exception
furnivall e826fee
help
furnivall c3c1d2e
should maybe reach the python now?
furnivall fedfe98
no i'm still dumb
furnivall 3627844
attempt with python instead of python3
furnivall d65a738
never mind i'm dumb as a rock
furnivall 8164712
python3
furnivall b42563a
i am so spectacularly dumb
furnivall d45a8b1
3.8/9 change for typing module
furnivall 39481ab
one more beep
furnivall 0f15117
from typing import List
furnivall 3152566
why is everything i'm doing so stupid
furnivall 325e59a
3.10
furnivall 02ea7d4
test infinity
furnivall 035bf7b
exit code for fail
furnivall 80abcbd
attempt to look at CI variable
furnivall 3a69909
3.10 again
furnivall b764d1f
removed errant try/catch blocks
furnivall 9bc4d82
attempt to get cvereport
furnivall 6dd487e
try/finally
furnivall 77dffbe
Merge branch 'master' into yarn-v4
furnivall File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import json | ||
from dataclasses import dataclass, asdict | ||
import subprocess | ||
import os | ||
|
||
@dataclass | ||
class Vulnerability: | ||
name: str | ||
id: str | ||
severity: str | ||
vulnerable_versions: str | ||
issue: str | ||
url: str | ||
tree_versions: list[str] | ||
dependents: list[str] | ||
|
||
def to_json(self): | ||
return asdict(self) | ||
|
||
def format_vulnerability(self) -> str: | ||
return ( | ||
f"├─ {self.name}\n" | ||
f"│ ├─ ID: {self.id if self.id is not None else 'N/A'}\n" | ||
f"│ ├─ URL: {self.url if self.url is not None else 'N/A'}\n" | ||
f"│ ├─ Issue: {self.issue if self.issue is not None else 'N/A'}\n" | ||
f"│ ├─ Severity: {self.severity if self.severity is not None else 'N/A'}\n" | ||
f"│ ├─ Vulnerable Versions: {self.vulnerable_versions if self.vulnerable_versions is not None else 'N/A'}\n" | ||
f"│ ├─ Tree Versions: {', '.join(self.tree_versions) if self.tree_versions else 'N/A'}\n" | ||
f"│ └─ Dependents: {', '.join(self.dependents) if self.dependents else 'N/A'}" | ||
) | ||
|
||
def __eq__(self, other): | ||
if isinstance(other, Vulnerability): | ||
return self.id == other.id | ||
# Compare based on id only - allows us to handle situations | ||
# where the other text of the vulnerability changes over time | ||
return False | ||
|
||
def run_audit_command(): | ||
command = ['yarn', 'npm', 'audit', '--recursive', '--environment', 'production', '--json'] | ||
result = subprocess.run(command, capture_output=True, text=True) | ||
# if any errors, print them and exit | ||
if result.stderr != '': | ||
print("Error running command `yarn npm audit --recursive --environment production --json` - Please raise a request in #platops-help. Error: ", repr(result.stderr)) | ||
exit(1) | ||
else: | ||
return result.stdout, result.returncode | ||
|
||
def validate_audit_response(response, exit_code): | ||
if exit_code == 0: | ||
print("No vulnerabilities found, nice work!") | ||
exit(0) | ||
list_of_issues = split_and_strip_json_blocks(response) | ||
print("Found", len(list_of_issues), "vulnerabilities") | ||
if check_json_keys(list_of_issues): | ||
vulnerabilities = build_list_of_issues(list_of_issues) | ||
return vulnerabilities | ||
else: | ||
print("Error parsing JSON returned from audit endpoint - please raise a request in #platops-help") | ||
exit(1) | ||
|
||
def split_and_strip_json_blocks(json_block): | ||
stripped_response = json_block.strip() | ||
list_of_issues = stripped_response.split('\n') | ||
return list_of_issues | ||
|
||
def check_yarn_audit_known_issues(): | ||
if os.path.exists('yarn-audit-known-issues'): | ||
print("Found yarn-audit-known-issues file - checking for suppressed vulnerabilities") | ||
with open('yarn-audit-known-issues') as f: | ||
known_issues_file_content = f.read() | ||
known_issues_stripped = split_and_strip_json_blocks(known_issues_file_content) | ||
if check_json_keys(known_issues_stripped): | ||
known_issues_list = build_list_of_issues(known_issues_stripped) | ||
return known_issues_list | ||
else : | ||
print("Error parsing JSON in your yarn-audit-known-issues file - delete the file and use the following command:") | ||
print("`yarn npm audit --recursive --environment production --json > yarn-audit-known-issues`") | ||
exit(1) | ||
else: | ||
return [] | ||
|
||
def check_json_keys(json_blocks): | ||
try: | ||
for block in json_blocks: | ||
data = json.loads(block) | ||
# Check if both 'value' and 'children' keys are in the JSON block (yarn v4 schema) | ||
if "value" not in data or "children" not in data: | ||
return False | ||
return True | ||
except json.JSONDecodeError: | ||
return False | ||
|
||
def combine_suppressions_and_vulnerabilities(suppressions, vulnerabilities): | ||
# comparison logic is based on the ID of the vulnerability - if the ID is the same, we assume it's the same | ||
# vulnerability | ||
unsuppressed_vulnerabilities = [item for item in vulnerabilities if item not in suppressions] | ||
unneeded_suppressions = [item for item in suppressions if item not in vulnerabilities] | ||
suppressed_active_vulnerabilities = [item for item in vulnerabilities if item in suppressions] | ||
return unsuppressed_vulnerabilities, unneeded_suppressions, suppressed_active_vulnerabilities | ||
|
||
|
||
def build_list_of_issues(json_blocks): | ||
""" | ||
Creates a list of Vulnerability objects from a list of JSON blocks. | ||
Each JSON block is parsed once and then used to construct a Vulnerability object. | ||
|
||
:param json_blocks: A list of JSON strings. | ||
:return: A list of Vulnerability objects. | ||
""" | ||
vulnerabilities = [] | ||
for block in json_blocks: | ||
parsed_data = json.loads(block) | ||
vuln = Vulnerability( | ||
name=parsed_data.get('value'), | ||
id=parsed_data.get('children', {}).get('ID'), | ||
issue=parsed_data.get('children', {}).get('Issue'), | ||
url=parsed_data.get('children', {}).get('URL'), | ||
severity=parsed_data.get('children', {}).get('Severity'), | ||
vulnerable_versions=parsed_data.get('children', {}).get('Vulnerable Versions'), | ||
tree_versions=parsed_data.get('children', {}).get('Tree Versions', []), | ||
dependents=parsed_data.get('children', {}).get('Dependents', []) | ||
) | ||
vulnerabilities.append(vuln) | ||
return vulnerabilities | ||
|
||
def print_vulnerabilities(vulnerabilities): | ||
print("Found", len(vulnerabilities), "active vulnerabilities, please fix these before pushing again.") | ||
for vulnerability in vulnerabilities: | ||
print(vulnerability.format_vulnerability()) | ||
|
||
def print_suppressions(suppressions): | ||
print("Found", len(suppressions), "unnecessary suppression(s), please check these are still needed." | ||
"If not, please remove them from your yarn-audit-known-issues file") | ||
for suppression in suppressions: | ||
print(suppression.format_vulnerability()) | ||
|
||
def decide_what_to_print(unsuppressed_vulnerabilities, unneeded_suppressions): | ||
if len(unsuppressed_vulnerabilities) == 0 and len(unneeded_suppressions) == 0: | ||
print("All vulnerabilities are suppressed and there are no unneeded_suppressions - no action required, nice work!") | ||
return | ||
if len(unsuppressed_vulnerabilities) > 0: | ||
print_vulnerabilities(unsuppressed_vulnerabilities) | ||
return | ||
if len(unneeded_suppressions) > 0: | ||
print_suppressions(unneeded_suppressions) | ||
|
||
def build_parent_json_for_cosmosDB(vulnerabilities, suppressions): | ||
if os.environ.get('CI') != 'true': | ||
print("Not running in CI, skipping parent JSON block") | ||
parent_block = {"suppressed_vulnerabilities": [s.to_json() for s in suppressions], | ||
"unsuppressed_vulnerabilities": [v.to_json() for v in vulnerabilities]} | ||
with open("audit-v4-cosmosdb-output", "w") as file: | ||
file.write(json.dumps(parent_block, indent=2)) | ||
|
||
audit_output, return_code = run_audit_command() | ||
vulnerabilities = validate_audit_response(audit_output, return_code) | ||
suppressions = check_yarn_audit_known_issues() | ||
unsuppressed_vulnerabilities, unneeded_suppressions, suppressed_active_vulnerabilities = combine_suppressions_and_vulnerabilities(suppressions, vulnerabilities) | ||
decide_what_to_print(unsuppressed_vulnerabilities, unneeded_suppressions) | ||
build_parent_json_for_cosmosDB(unsuppressed_vulnerabilities, suppressed_active_vulnerabilities) | ||
if len(unsuppressed_vulnerabilities) > 0: | ||
exit(1) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this doesn't look like it skips anything?