guarddog 2.7.1__py3-none-any.whl → 2.9.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.
Files changed (47) hide show
  1. guarddog/analyzer/metadata/__init__.py +3 -0
  2. guarddog/analyzer/metadata/go/typosquatting.py +11 -28
  3. guarddog/analyzer/metadata/npm/direct_url_dependency.py +0 -1
  4. guarddog/analyzer/metadata/npm/typosquatting.py +24 -59
  5. guarddog/analyzer/metadata/pypi/repository_integrity_mismatch.py +53 -164
  6. guarddog/analyzer/metadata/pypi/typosquatting.py +20 -77
  7. guarddog/analyzer/metadata/repository_integrity_mismatch.py +202 -2
  8. guarddog/analyzer/metadata/resources/top_go_packages.json +2926 -2923
  9. guarddog/analyzer/metadata/resources/top_npm_packages.json +8005 -8002
  10. guarddog/analyzer/metadata/resources/top_pypi_packages.json +15003 -60021
  11. guarddog/analyzer/metadata/resources/top_rubygems_packages.json +979 -0
  12. guarddog/analyzer/metadata/rubygems/__init__.py +26 -0
  13. guarddog/analyzer/metadata/rubygems/bundled_binary.py +13 -0
  14. guarddog/analyzer/metadata/rubygems/empty_information.py +24 -0
  15. guarddog/analyzer/metadata/rubygems/release_zero.py +22 -0
  16. guarddog/analyzer/metadata/rubygems/repository_integrity_mismatch.py +49 -0
  17. guarddog/analyzer/metadata/rubygems/typosquatting.py +91 -0
  18. guarddog/analyzer/metadata/typosquatting.py +218 -0
  19. guarddog/analyzer/metadata/utils.py +23 -0
  20. guarddog/analyzer/sourcecode/__init__.py +2 -0
  21. guarddog/analyzer/sourcecode/api-obfuscation.yml +35 -40
  22. guarddog/analyzer/sourcecode/code-execution.yml +20 -0
  23. guarddog/analyzer/sourcecode/exec-base64.yml +19 -0
  24. guarddog/analyzer/sourcecode/exfiltrate-sensitive-data.yml +31 -5
  25. guarddog/analyzer/sourcecode/npm-api-obfuscation.yml +51 -0
  26. guarddog/analyzer/sourcecode/rubygems-code-execution.yml +67 -0
  27. guarddog/analyzer/sourcecode/rubygems-exec-base64.yml +26 -0
  28. guarddog/analyzer/sourcecode/rubygems-exfiltrate-sensitive-data.yml +70 -0
  29. guarddog/analyzer/sourcecode/rubygems-install-hook.yml +45 -0
  30. guarddog/analyzer/sourcecode/rubygems-network-on-require.yml +78 -0
  31. guarddog/analyzer/sourcecode/rubygems-serialize-environment.yml +38 -0
  32. guarddog/analyzer/sourcecode/screenshot.yml +38 -0
  33. guarddog/ecosystems.py +3 -0
  34. guarddog/scanners/__init__.py +6 -0
  35. guarddog/scanners/npm_project_scanner.py +1 -1
  36. guarddog/scanners/rubygems_package_scanner.py +112 -0
  37. guarddog/scanners/rubygems_project_scanner.py +75 -0
  38. guarddog/scanners/scanner.py +36 -12
  39. guarddog/utils/archives.py +1 -1
  40. guarddog-2.9.0.dist-info/METADATA +471 -0
  41. {guarddog-2.7.1.dist-info → guarddog-2.9.0.dist-info}/RECORD +46 -29
  42. {guarddog-2.7.1.dist-info → guarddog-2.9.0.dist-info}/WHEEL +1 -1
  43. guarddog-2.7.1.dist-info/METADATA +0 -40
  44. {guarddog-2.7.1.dist-info → guarddog-2.9.0.dist-info}/entry_points.txt +0 -0
  45. {guarddog-2.7.1.dist-info → guarddog-2.9.0.dist-info}/licenses/LICENSE +0 -0
  46. {guarddog-2.7.1.dist-info → guarddog-2.9.0.dist-info}/licenses/LICENSE-3rdparty.csv +0 -0
  47. {guarddog-2.7.1.dist-info → guarddog-2.9.0.dist-info}/licenses/NOTICE +0 -0
@@ -21,13 +21,13 @@ rules:
21
21
  regex: ([\"\'].*(.aws/credentials|.docker/config.json)[\"\'])
22
22
  - patterns:
23
23
  - pattern-either:
24
- - pattern: os.getenv($ENVVAR)
24
+ - pattern: os.getenv($ENVVAR, ...)
25
25
  - pattern: os.environ[$ENVVAR]
26
- - pattern: os.environ.get($ENVVAR)
26
+ - pattern: os.environ.get($ENVVAR, ...)
27
27
 
28
- - pattern: getenv($ENVVAR)
28
+ - pattern: getenv($ENVVAR, ...)
29
29
  - pattern: environ[$ENVVAR]
30
- - pattern: environ.get($ENVVAR)
30
+ - pattern: environ.get($ENVVAR, ...)
31
31
  - metavariable-regex:
32
32
  metavariable: $ENVVAR
33
33
  regex: ([\"\'](AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN)[\"\'])
@@ -54,6 +54,32 @@ rules:
54
54
  - pattern-inside: $S = socket.socket(...); ...
55
55
  - pattern-inside: $S.connect(...); ...
56
56
  - pattern-inside: $S.send(...)
57
+ # DNS exfiltration - socket module (built-in)
58
+ - pattern-inside: socket.gethostbyname(...)
59
+ - pattern-inside: socket.getaddrinfo(...)
60
+ - pattern-inside: socket.gethostbyname_ex(...)
61
+ # DNS exfiltration - dnspython library (direct calls)
62
+ - pattern-inside: dns.resolver.query(...)
63
+ - pattern-inside: dns.resolver.resolve(...)
64
+ - pattern-inside: dns.query.udp(...)
65
+ - pattern-inside: dns.query.tcp(...)
66
+ # DNS exfiltration - dnspython Resolver instance
67
+ - patterns:
68
+ - pattern-inside: |
69
+ $R = dns.resolver.Resolver()
70
+ ...
71
+ - pattern-inside: $R.query(...)
72
+ - patterns:
73
+ - pattern-inside: |
74
+ $R = dns.resolver.Resolver()
75
+ ...
76
+ - pattern-inside: $R.resolve(...)
77
+ # DNS exfiltration - aiodns (async)
78
+ - patterns:
79
+ - pattern-inside: |
80
+ $R = aiodns.DNSResolver()
81
+ ...
82
+ - pattern-inside: $R.query(...)
57
83
  languages:
58
84
  - python
59
- severity: WARNING
85
+ severity: WARNING
@@ -0,0 +1,51 @@
1
+ rules:
2
+ - id: npm-api-obfuscation
3
+ languages:
4
+ - javascript
5
+ message: This package uses obfuscated API calls that may evade static analysis detection
6
+ metadata:
7
+ description: Identify obfuscated API calls using alternative JS syntax patterns
8
+ severity: WARNING
9
+ patterns:
10
+ - pattern-either:
11
+ # Covered cases:
12
+ # 1) module["function"]()
13
+ # 2) Reflect.get(module, "function")()
14
+ # 3) Object.getOwnPropertyDescriptor(module, "function").value()
15
+ # 4) module[Object.getOwnPropertyNames(module).find(name => name === "function")]()
16
+ # 5) module[Object.keys(module).find(name => name === "function")]()
17
+ # 6) Object.entries(module).find(([name, value]) => name === "function")[1]()
18
+ # 7) Object.entries(module).filter(([k]) => k === 'function')[0][1]()
19
+ # Each of these can also use .call(), .apply(), .bind() variants as well:
20
+ # e.g., module["function"].call({}, ...), module["function"].apply({}, [...]), module["function"].bind({})(...)
21
+ - pattern: $MODULE[$FUNCTION]($...ARGS)
22
+ - pattern: $MODULE[$FUNCTION].call($...ARGS)
23
+ - pattern: $MODULE[$FUNCTION].apply($...ARGS)
24
+ - pattern: $MODULE[$FUNCTION].bind($...ARGS)()
25
+ - pattern: Reflect.get($MODULE, $FUNCTION)($...ARGS)
26
+ - pattern: Reflect.get($MODULE, $FUNCTION).call($...ARGS)
27
+ - pattern: Reflect.get($MODULE, $FUNCTION).apply($...ARGS)
28
+ - pattern: Reflect.get($MODULE, $FUNCTION).bind($...ARGS)()
29
+ - pattern: Object.getOwnPropertyDescriptor($MODULE, $FUNCTION).value($...ARGS)
30
+ - pattern: Object.getOwnPropertyDescriptor($MODULE, $FUNCTION).value.call($...ARGS)
31
+ - pattern: Object.getOwnPropertyDescriptor($MODULE, $FUNCTION).value.apply($...ARGS)
32
+ - pattern: Object.getOwnPropertyDescriptor($MODULE, $FUNCTION).value.bind($...ARGS)()
33
+ - pattern: $MODULE[Object.getOwnPropertyNames($MODULE).find($VAR => $VAR === $FUNCTION)]($...ARGS)
34
+ - pattern: $MODULE[Object.getOwnPropertyNames($MODULE).find($VAR => $VAR === $FUNCTION)].call($...ARGS)
35
+ - pattern: $MODULE[Object.getOwnPropertyNames($MODULE).find($VAR => $VAR === $FUNCTION)].apply($...ARGS)
36
+ - pattern: $MODULE[Object.getOwnPropertyNames($MODULE).find($VAR => $VAR === $FUNCTION)].bind($...ARGS)()
37
+ - pattern: $MODULE[Object.keys($MODULE).find($VAR => $VAR === $FUNCTION)]($...ARGS)
38
+ - pattern: $MODULE[Object.keys($MODULE).find($VAR => $VAR === $FUNCTION)].call($...ARGS)
39
+ - pattern: $MODULE[Object.keys($MODULE).find($VAR => $VAR === $FUNCTION)].apply($...ARGS)
40
+ - pattern: $MODULE[Object.keys($MODULE).find($VAR => $VAR === $FUNCTION)].bind($...ARGS)()
41
+ - pattern: Object.entries($MODULE).find(([$VAR, $VALUE]) => $VAR === $FUNCTION)[1]($...ARGS)
42
+ - pattern: Object.entries($MODULE).find(([$VAR, $VALUE]) => $VAR === $FUNCTION)[1].call($...ARGS)
43
+ - pattern: Object.entries($MODULE).find(([$VAR, $VALUE]) => $VAR === $FUNCTION)[1].apply($...ARGS)
44
+ - pattern: Object.entries($MODULE).find(([$VAR, $VALUE]) => $VAR === $FUNCTION)[1].bind($...ARGS)()
45
+ - pattern: Object.entries($MODULE).filter(([$VAR]) => $VAR === $FUNCTION)[0][1]($...ARGS)
46
+ - pattern: Object.entries($MODULE).filter(([$VAR]) => $VAR === $FUNCTION)[0][1].call($...ARGS)
47
+ - pattern: Object.entries($MODULE).filter(([$VAR]) => $VAR === $FUNCTION)[0][1].apply($...ARGS)
48
+ - pattern: Object.entries($MODULE).filter(([$VAR]) => $VAR === $FUNCTION)[0][1].bind($...ARGS)()
49
+ - metavariable-regex:
50
+ metavariable: $MODULE
51
+ regex: "^[A-Za-z_][A-Za-z0-9_]*$"
@@ -0,0 +1,67 @@
1
+ rules:
2
+ - id: rubygems-code-execution
3
+ languages:
4
+ - ruby
5
+ message: |
6
+ This package executes OS commands. While this can be legitimate,
7
+ it is commonly used by malicious packages to run arbitrary code.
8
+ metadata:
9
+ description: Identify when a gem executes OS commands
10
+ patterns:
11
+ - pattern-either:
12
+ # Kernel methods for command execution
13
+ - pattern: system(...)
14
+ - pattern: exec(...)
15
+ - pattern: spawn(...)
16
+ - pattern: Kernel.system(...)
17
+ - pattern: Kernel.exec(...)
18
+ - pattern: Kernel.spawn(...)
19
+
20
+ # Backtick execution - semgrep uses pattern for this
21
+ - pattern: "`...`"
22
+
23
+ # %x{} command execution
24
+ - pattern: "%x{...}"
25
+ - pattern: "%x[...]"
26
+ - pattern: "%x(...)"
27
+
28
+ # Open3 module
29
+ - pattern: Open3.capture2(...)
30
+ - pattern: Open3.capture2e(...)
31
+ - pattern: Open3.capture3(...)
32
+ - pattern: Open3.pipeline(...)
33
+ - pattern: Open3.pipeline_r(...)
34
+ - pattern: Open3.pipeline_rw(...)
35
+ - pattern: Open3.pipeline_start(...)
36
+ - pattern: Open3.pipeline_w(...)
37
+ - pattern: Open3.popen2(...)
38
+ - pattern: Open3.popen2e(...)
39
+ - pattern: Open3.popen3(...)
40
+
41
+ # IO.popen
42
+ - pattern: IO.popen(...)
43
+
44
+ # Process.spawn
45
+ - pattern: Process.spawn(...)
46
+ - pattern: Process.exec(...)
47
+
48
+ # PTY for pseudo-terminal
49
+ - pattern: PTY.spawn(...)
50
+
51
+ # eval with dynamic content
52
+ - patterns:
53
+ - pattern-either:
54
+ - pattern: eval($ARG)
55
+ - pattern: instance_eval($ARG)
56
+ - pattern: class_eval($ARG)
57
+ - pattern: module_eval($ARG)
58
+ - pattern-not: eval("...")
59
+ - pattern-not: instance_eval("...")
60
+ - pattern-not: class_eval("...")
61
+ - pattern-not: module_eval("...")
62
+ severity: WARNING
63
+ paths:
64
+ include:
65
+ - "*.gemspec"
66
+ - "**/extconf.rb"
67
+ - "**/Rakefile"
@@ -0,0 +1,26 @@
1
+ rules:
2
+ - id: rubygems-exec-base64
3
+ languages:
4
+ - ruby
5
+ message: |
6
+ This package contains a call to eval with base64-decoded content.
7
+ This is a common method to hide malicious payloads from static analysis.
8
+ metadata:
9
+ description: Identify when a package dynamically executes base64-encoded code
10
+ mode: taint
11
+ pattern-sources:
12
+ - pattern-either:
13
+ - pattern: Base64.decode64(...)
14
+ - pattern: Base64.strict_decode64(...)
15
+ - pattern: Base64.urlsafe_decode64(...)
16
+ - pattern: $X.unpack("m")
17
+ - pattern: $X.unpack("m0")
18
+ pattern-sinks:
19
+ - pattern-either:
20
+ - pattern: eval(...)
21
+ - pattern: instance_eval(...)
22
+ - pattern: class_eval(...)
23
+ - pattern: module_eval(...)
24
+ - pattern: Kernel.eval(...)
25
+ - pattern: binding.eval(...)
26
+ severity: WARNING
@@ -0,0 +1,70 @@
1
+ rules:
2
+ - id: rubygems-exfiltrate-sensitive-data
3
+ languages:
4
+ - ruby
5
+ mode: taint
6
+ message: |
7
+ This package reads sensitive data and sends it to a remote server.
8
+ This could indicate credential theft or data exfiltration.
9
+ metadata:
10
+ description: Identify when a package reads and exfiltrates sensitive data from the local system
11
+ pattern-sources:
12
+ - pattern-either:
13
+ # Environment variables
14
+ - pattern: ENV
15
+ - pattern: ENV[...]
16
+ - pattern: ENV.fetch(...)
17
+ - pattern: ENV.to_h
18
+ - pattern: ENV.to_hash
19
+
20
+ # Specific sensitive env vars
21
+ - pattern: ENV['HOME']
22
+ - pattern: ENV['USER']
23
+ - pattern: ENV['USERNAME']
24
+ - pattern: ENV['AWS_ACCESS_KEY_ID']
25
+ - pattern: ENV['AWS_SECRET_ACCESS_KEY']
26
+ - pattern: ENV['AWS_SESSION_TOKEN']
27
+ - pattern: ENV['GITHUB_TOKEN']
28
+ - pattern: ENV['GH_TOKEN']
29
+
30
+ # System info
31
+ - pattern: Socket.gethostname
32
+ - pattern: Etc.getlogin
33
+ - pattern: Etc.getpwuid(...)
34
+
35
+ # Reading sensitive files
36
+ - pattern: File.read("~/.ssh/...")
37
+ - pattern: File.read("~/.aws/...")
38
+ - pattern: File.read("~/.netrc")
39
+ - pattern: File.read("~/.git-credentials")
40
+
41
+ # Dir patterns for sensitive locations
42
+ - pattern: Dir.home
43
+ - pattern: Dir.glob("~/.ssh/*")
44
+ - pattern: Dir.glob("~/.aws/*")
45
+ pattern-sinks:
46
+ - pattern-either:
47
+ # Net::HTTP
48
+ - pattern: Net::HTTP.post(...)
49
+ - pattern: Net::HTTP.post_form(...)
50
+ - pattern: $HTTP.request(...)
51
+ - pattern: $HTTP.post(...)
52
+ - pattern: $HTTP.put(...)
53
+
54
+ # open-uri
55
+ - pattern: URI.open(...)
56
+ - pattern: OpenURI.open_uri(...)
57
+
58
+ # HTTParty, Faraday, RestClient
59
+ - pattern: HTTParty.post(...)
60
+ - pattern: HTTParty.put(...)
61
+ - pattern: Faraday.post(...)
62
+ - pattern: Faraday.put(...)
63
+ - pattern: RestClient.post(...)
64
+ - pattern: RestClient.put(...)
65
+
66
+ # Socket
67
+ - pattern: $SOCKET.write(...)
68
+ - pattern: $SOCKET.send(...)
69
+ - pattern: TCPSocket.new(...)
70
+ severity: WARNING
@@ -0,0 +1,45 @@
1
+ rules:
2
+ - id: rubygems-install-hook
3
+ languages:
4
+ - ruby
5
+ message: |
6
+ This package uses Gem::Installer hooks which execute code during gem installation.
7
+ This is a common technique for malicious gems to run code when installed.
8
+ metadata:
9
+ description: Identify when a gem registers installation hooks
10
+ patterns:
11
+ - pattern-either:
12
+ # Post-install hooks
13
+ - pattern: Gem.post_install(...)
14
+ - pattern: Gem.post_install { ... }
15
+ - pattern: Gem.post_install do ... end
16
+ - pattern: Gem::Installer.post_install(...)
17
+ - pattern: Gem::Installer.post_install { ... }
18
+ - pattern: Gem::Installer.post_install do ... end
19
+
20
+ # Pre-install hooks
21
+ - pattern: Gem.pre_install(...)
22
+ - pattern: Gem.pre_install { ... }
23
+ - pattern: Gem.pre_install do ... end
24
+ - pattern: Gem::Installer.pre_install(...)
25
+ - pattern: Gem::Installer.pre_install { ... }
26
+ - pattern: Gem::Installer.pre_install do ... end
27
+
28
+ # Post-uninstall hooks
29
+ - pattern: Gem.post_uninstall(...)
30
+ - pattern: Gem.post_uninstall { ... }
31
+ - pattern: Gem.post_uninstall do ... end
32
+
33
+ # Pre-uninstall hooks
34
+ - pattern: Gem.pre_uninstall(...)
35
+ - pattern: Gem.pre_uninstall { ... }
36
+ - pattern: Gem.pre_uninstall do ... end
37
+
38
+ # Extension building (can run arbitrary code)
39
+ - pattern: |
40
+ Gem::Specification.new do |$S|
41
+ ...
42
+ $S.extensions = ...
43
+ ...
44
+ end
45
+ severity: WARNING
@@ -0,0 +1,78 @@
1
+ rules:
2
+ - id: rubygems-network-on-require
3
+ languages:
4
+ - ruby
5
+ message: |
6
+ This package makes network requests at the top level, which means it runs
7
+ when the gem is required. Malicious gems often use this to phone home or
8
+ download additional payloads.
9
+ metadata:
10
+ description: Identify when a gem makes network requests when required
11
+ patterns:
12
+ - pattern-either:
13
+ # Net::HTTP at top level
14
+ - pattern: |
15
+ require 'net/http'
16
+ ...
17
+ Net::HTTP.$METHOD(...)
18
+ - pattern: |
19
+ require "net/http"
20
+ ...
21
+ Net::HTTP.$METHOD(...)
22
+
23
+ # open-uri at top level
24
+ - pattern: |
25
+ require 'open-uri'
26
+ ...
27
+ URI.open(...)
28
+ - pattern: |
29
+ require "open-uri"
30
+ ...
31
+ URI.open(...)
32
+ - pattern: |
33
+ require 'open-uri'
34
+ ...
35
+ open(...)
36
+ - pattern: |
37
+ require "open-uri"
38
+ ...
39
+ open(...)
40
+
41
+ # HTTParty at top level
42
+ - pattern: |
43
+ require 'httparty'
44
+ ...
45
+ HTTParty.$METHOD(...)
46
+ - pattern: |
47
+ require "httparty"
48
+ ...
49
+ HTTParty.$METHOD(...)
50
+
51
+ # Faraday at top level
52
+ - pattern: |
53
+ require 'faraday'
54
+ ...
55
+ Faraday.$METHOD(...)
56
+ - pattern: |
57
+ require "faraday"
58
+ ...
59
+ Faraday.$METHOD(...)
60
+
61
+ # Socket connections at top level
62
+ - pattern: |
63
+ require 'socket'
64
+ ...
65
+ TCPSocket.new(...)
66
+ - pattern: |
67
+ require "socket"
68
+ ...
69
+ TCPSocket.new(...)
70
+ - pattern: |
71
+ require 'socket'
72
+ ...
73
+ TCPSocket.open(...)
74
+ - pattern: |
75
+ require "socket"
76
+ ...
77
+ TCPSocket.open(...)
78
+ severity: WARNING
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: rubygems-serialize-environment
3
+ languages:
4
+ - ruby
5
+ message: |
6
+ This package serializes the entire ENV hash, which may indicate
7
+ an attempt to steal environment variables including secrets and credentials.
8
+ metadata:
9
+ description: Identify when a package serializes ENV to exfiltrate environment variables
10
+ patterns:
11
+ - pattern-either:
12
+ # JSON serialization
13
+ - pattern: ENV.to_h.to_json
14
+ - pattern: ENV.to_hash.to_json
15
+ - pattern: JSON.dump(ENV)
16
+ - pattern: JSON.dump(ENV.to_h)
17
+ - pattern: JSON.dump(ENV.to_hash)
18
+ - pattern: JSON.generate(ENV)
19
+ - pattern: JSON.generate(ENV.to_h)
20
+ - pattern: JSON.generate(ENV.to_hash)
21
+
22
+ # YAML serialization
23
+ - pattern: ENV.to_h.to_yaml
24
+ - pattern: ENV.to_hash.to_yaml
25
+ - pattern: YAML.dump(ENV)
26
+ - pattern: YAML.dump(ENV.to_h)
27
+ - pattern: YAML.dump(ENV.to_hash)
28
+
29
+ # Marshal serialization
30
+ - pattern: Marshal.dump(ENV)
31
+ - pattern: Marshal.dump(ENV.to_h)
32
+ - pattern: Marshal.dump(ENV.to_hash)
33
+
34
+ # Converting to string for sending
35
+ - pattern: ENV.to_h.inspect
36
+ - pattern: ENV.to_hash.inspect
37
+ - pattern: ENV.inspect
38
+ severity: WARNING
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: screenshot
3
+ languages:
4
+ - python
5
+ message: This package is taking screenshots, which can be used to steal sensitive information displayed on screen
6
+ metadata:
7
+ description: Identify when a package captures screenshots of the user's display
8
+ patterns:
9
+ - pattern-either:
10
+ # PIL ImageGrab
11
+ - pattern: ImageGrab.grab(...)
12
+ - pattern: PIL.ImageGrab.grab(...)
13
+
14
+ # pyscreenshot library
15
+ - pattern: pyscreenshot.grab(...)
16
+
17
+ # pyautogui library
18
+ - pattern: pyautogui.screenshot(...)
19
+
20
+ # mss library - various patterns
21
+ - pattern: mss.mss().grab(...)
22
+ - pattern: |
23
+ with mss.mss() as $SCT:
24
+ ...
25
+ $SCT.grab(...)
26
+ - pattern: |
27
+ $SCT = mss.mss()
28
+ ...
29
+ $SCT.grab(...)
30
+ - pattern: $MSS.grab(...)
31
+
32
+ # D3DShot (Windows DirectX screenshots)
33
+ - pattern: d3dshot.create(...).screenshot(...)
34
+ - pattern: |
35
+ $D3D = d3dshot.create(...)
36
+ ...
37
+ $D3D.screenshot(...)
38
+ severity: WARNING
guarddog/ecosystems.py CHANGED
@@ -7,6 +7,7 @@ class ECOSYSTEM(Enum):
7
7
  GO = "go"
8
8
  GITHUB_ACTION = "github-action"
9
9
  EXTENSION = "extension"
10
+ RUBYGEMS = "rubygems"
10
11
 
11
12
 
12
13
  def get_friendly_name(ecosystem: ECOSYSTEM) -> str:
@@ -21,5 +22,7 @@ def get_friendly_name(ecosystem: ECOSYSTEM) -> str:
21
22
  return "GitHub Action"
22
23
  case ECOSYSTEM.EXTENSION:
23
24
  return "Extension"
25
+ case ECOSYSTEM.RUBYGEMS:
26
+ return "RubyGems"
24
27
  case _:
25
28
  return ecosystem.value
@@ -9,6 +9,8 @@ from .go_package_scanner import GoModuleScanner
9
9
  from .go_project_scanner import GoDependenciesScanner
10
10
  from .github_action_scanner import GithubActionScanner
11
11
  from .extension_scanner import ExtensionScanner
12
+ from .rubygems_package_scanner import RubyGemsPackageScanner
13
+ from .rubygems_project_scanner import RubyGemsRequirementsScanner
12
14
  from .scanner import PackageScanner, ProjectScanner
13
15
  from ..ecosystems import ECOSYSTEM
14
16
 
@@ -36,6 +38,8 @@ def get_package_scanner(ecosystem: ECOSYSTEM) -> Optional[PackageScanner]:
36
38
  return GithubActionScanner()
37
39
  case ECOSYSTEM.EXTENSION:
38
40
  return ExtensionScanner()
41
+ case ECOSYSTEM.RUBYGEMS:
42
+ return RubyGemsPackageScanner()
39
43
  return None
40
44
 
41
45
 
@@ -62,4 +66,6 @@ def get_project_scanner(ecosystem: ECOSYSTEM) -> Optional[ProjectScanner]:
62
66
  return GitHubActionDependencyScanner()
63
67
  case ECOSYSTEM.EXTENSION:
64
68
  return None # we're not including dependency scanning for this PR
69
+ case ECOSYSTEM.RUBYGEMS:
70
+ return RubyGemsRequirementsScanner()
65
71
  return None
@@ -5,7 +5,7 @@ import re
5
5
  from typing import List
6
6
 
7
7
  import requests
8
- from semantic_version import NpmSpec, Version # type:ignore
8
+ from semantic_version import NpmSpec, Version # type: ignore
9
9
 
10
10
  from guarddog.scanners.npm_package_scanner import NPMPackageScanner
11
11
  from guarddog.scanners.scanner import Dependency, DependencyVersion, ProjectScanner
@@ -0,0 +1,112 @@
1
+ import logging
2
+ import os
3
+ from typing import Tuple
4
+
5
+ import requests
6
+
7
+ from guarddog.analyzer.analyzer import Analyzer
8
+ from guarddog.ecosystems import ECOSYSTEM
9
+ from guarddog.scanners.scanner import PackageScanner
10
+ from guarddog.utils.archives import safe_extract
11
+
12
+ log = logging.getLogger("guarddog")
13
+
14
+ RUBYGEMS_API_URL = "https://rubygems.org/api/v1"
15
+
16
+
17
+ class RubyGemsPackageScanner(PackageScanner):
18
+ def __init__(self) -> None:
19
+ super().__init__(Analyzer(ECOSYSTEM.RUBYGEMS))
20
+
21
+ def _extract_archive(self, archive_path: str, target_path: str) -> None:
22
+ """
23
+ Override to handle .gem files which are nested tar archives.
24
+ The outer tar contains data.tar.gz which has the actual source code.
25
+
26
+ Args:
27
+ archive_path (str): path to the .gem file
28
+ target_path (str): directory to extract the source code into
29
+ """
30
+ if not archive_path.endswith(".gem"):
31
+ # Fall back to default behavior for non-gem archives
32
+ super()._extract_archive(archive_path, target_path)
33
+ return
34
+
35
+ os.makedirs(target_path, exist_ok=True)
36
+
37
+ # Extract outer .gem archive to a temporary location
38
+ outer_extract = os.path.join(target_path, "_gem_contents")
39
+ os.makedirs(outer_extract, exist_ok=True)
40
+
41
+ log.debug(f"Extracting outer gem archive {archive_path}")
42
+ safe_extract(archive_path, outer_extract)
43
+
44
+ # Find the inner data archive (data.tar.gz or data.tar)
45
+ data_tar_path = os.path.join(outer_extract, "data.tar.gz")
46
+ if not os.path.exists(data_tar_path):
47
+ data_tar_path = os.path.join(outer_extract, "data.tar")
48
+
49
+ if not os.path.exists(data_tar_path):
50
+ raise Exception(f"data.tar.gz not found in gem {archive_path}")
51
+
52
+ # Extract the inner data archive to the final target
53
+ log.debug(f"Extracting inner data archive {data_tar_path}")
54
+ safe_extract(data_tar_path, target_path)
55
+
56
+ log.debug(f"Successfully extracted gem files to {target_path}")
57
+
58
+ def download_and_get_package_info(
59
+ self, directory: str, package_name: str, version=None
60
+ ) -> Tuple[dict, str]:
61
+ gem_info = self._get_gem_info(package_name)
62
+
63
+ if version is None:
64
+ version = gem_info["version"]
65
+
66
+ extract_dir = self._download_gem(package_name, version, directory)
67
+ return gem_info, extract_dir
68
+
69
+ def _get_gem_info(self, package_name: str) -> dict:
70
+ url = f"{RUBYGEMS_API_URL}/gems/{package_name}.json"
71
+ log.debug(f"Fetching gem info from {url}")
72
+ response = requests.get(url)
73
+ response.raise_for_status()
74
+ return response.json()
75
+
76
+ def _get_gem_version_info(self, package_name: str, version: str) -> dict:
77
+ url = f"{RUBYGEMS_API_URL}/versions/{package_name}.json"
78
+ log.debug(f"Fetching version info from {url}")
79
+ response = requests.get(url)
80
+ response.raise_for_status()
81
+
82
+ versions = response.json()
83
+ for v in versions:
84
+ if v["number"] == version:
85
+ return v
86
+
87
+ raise Exception(f"Version {version} for gem {package_name} not found")
88
+
89
+ def _download_gem(self, package_name: str, version: str, directory: str) -> str:
90
+ """
91
+ Downloads and extracts a RubyGem package.
92
+
93
+ Uses the parent class's download_compressed method which will call our
94
+ overridden _extract_archive method to handle the nested .gem format.
95
+
96
+ Args:
97
+ package_name (str): name of the gem
98
+ version (str): version of the gem
99
+ directory (str): directory to download and extract to
100
+
101
+ Returns:
102
+ str: path to the extracted gem contents
103
+ """
104
+ gem_url = f"https://rubygems.org/gems/{package_name}-{version}.gem"
105
+ gem_path = os.path.join(directory, f"{package_name}-{version}.gem")
106
+ extract_dir = os.path.join(directory, package_name)
107
+
108
+ # Use parent class method which handles download and extraction
109
+ # The extraction will use our overridden _extract_archive method
110
+ self.download_compressed(gem_url, gem_path, extract_dir)
111
+
112
+ return extract_dir