1
+ # frozen_string_literal: true
2
+
3
+ module Ecosystem
4
+ class Terraform < Base
5
+
6
+ def registry_url ( package , version = nil )
7
+ base_url = "https://registry.terraform.io/modules"
8
+ parts = package . name . split ( '/' )
9
+ if parts . length == 3
10
+ namespace , name , provider = parts
11
+ "#{ base_url } /#{ namespace } /#{ name } /#{ provider } " + ( version ? "/#{ version } " : "" )
12
+ else
13
+ "#{ base_url } /#{ package . name } "
14
+ end
15
+ end
16
+
17
+ def download_url ( package , version = nil )
18
+ return nil unless version . present?
19
+
20
+ parts = package . name . split ( '/' )
21
+ return nil unless parts . length == 3
22
+
23
+ namespace , name , provider = parts
24
+ "https://registry.terraform.io/v1/modules/#{ namespace } /#{ name } /#{ provider } /#{ version } /download"
25
+ end
26
+
27
+ def documentation_url ( package , _version = nil )
28
+ registry_url ( package )
29
+ end
30
+
31
+ def install_command ( package , version = nil )
32
+ parts = package . name . split ( '/' )
33
+ return nil unless parts . length == 3
34
+
35
+ module_source = package . name
36
+ module_source += "?version=#{ version } " if version
37
+
38
+ "terraform init # with module \" example\" { source = \" #{ module_source } \" }"
39
+ end
40
+
41
+ def check_status_url ( package )
42
+ parts = package . name . split ( '/' )
43
+ return nil unless parts . length == 3
44
+
45
+ namespace , name , provider = parts
46
+ "https://registry.terraform.io/v1/modules/#{ namespace } /#{ name } /#{ provider } /versions"
47
+ end
48
+
49
+ def check_status ( package )
50
+ url = check_status_url ( package )
51
+ return nil unless url
52
+
53
+ begin
54
+ json = get_json ( url )
55
+ versions = json . dig ( 'modules' , 0 , 'versions' )
56
+ return "removed" if !versions || versions . empty?
57
+ nil
58
+ rescue StandardError
59
+ "removed"
60
+ end
61
+ end
62
+
63
+ def all_package_names
64
+ # Use the v2 API that the Terraform Registry website uses
65
+ all_names = [ ]
66
+ page = 1
67
+ page_size = 100
68
+
69
+ loop do
70
+ begin
71
+ response = get_json ( "https://registry.terraform.io/v2/modules?page[size]=#{ page_size } &page[number]=#{ page } " )
72
+ modules = response . dig ( 'data' ) || [ ]
73
+
74
+ break if modules . empty?
75
+
76
+ modules . each do |mod |
77
+ full_name = mod . dig ( 'attributes' , 'full-name' )
78
+ all_names << full_name if full_name
79
+ end
80
+
81
+ # Check if there's a next page
82
+ next_page = response . dig ( 'links' , 'next' )
83
+ break unless next_page
84
+
85
+ page += 1
86
+
87
+ # Safety break to prevent infinite loops (limit to first 50 pages)
88
+ break if page > 50
89
+ rescue
90
+ break
91
+ end
92
+ end
93
+
94
+ all_names . uniq
95
+ rescue
96
+ [ ]
97
+ end
98
+
99
+ def recently_updated_package_names
100
+ # Get recently updated modules using v2 API sorted by recent activity
101
+ begin
102
+ response = get_json ( "https://registry.terraform.io/v2/modules?include=latest-version&page[size]=100&page[number]=1&sort=-updated" )
103
+ modules = response . dig ( 'data' ) || [ ]
104
+
105
+ modules . map do |mod |
106
+ mod . dig ( 'attributes' , 'full-name' )
107
+ end . compact
108
+ rescue
109
+ [ ]
110
+ end
111
+ end
112
+
113
+ def fetch_package_metadata ( name )
114
+ parts = name . split ( '/' )
115
+ return nil unless parts . length == 3
116
+
117
+ namespace , module_name , provider = parts
118
+ get_json ( "https://registry.terraform.io/v1/modules/#{ namespace } /#{ module_name } /#{ provider } " )
119
+ rescue
120
+ nil
121
+ end
122
+
123
+ def map_package_metadata ( package )
124
+ return false unless package . present?
125
+
126
+ {
127
+ name : package [ 'id' ] ,
128
+ description : package [ 'description' ] ,
129
+ homepage : repo_fallback ( package [ 'source' ] , nil ) ,
130
+ repository_url : repo_fallback ( package [ 'source' ] , nil ) ,
131
+ keywords_array : [ ] ,
132
+ licenses : 'Unknown' ,
133
+ namespace : package [ 'namespace' ] ,
134
+ downloads : package [ 'downloads' ] || 0 ,
135
+ downloads_period : 'total' ,
136
+ versions : package [ 'versions' ] || [ ] ,
137
+ metadata : {
138
+ 'provider' => package [ 'provider' ] ,
139
+ 'verified' => package [ 'verified' ] || false ,
140
+ 'trusted' => package [ 'trusted' ] || false ,
141
+ 'latest_version' => package [ 'versions' ] &.first . is_a? ( Hash ) ? package [ 'versions' ] . first [ 'version' ] : package [ 'versions' ] &.first ,
142
+ 'owner' => package [ 'owner' ]
143
+ }
144
+ }
145
+ end
146
+
147
+ def versions_metadata ( pkg_metadata , existing_version_numbers = [ ] )
148
+ return [ ] unless pkg_metadata [ :versions ]
149
+
150
+ pkg_metadata [ :versions ]
151
+ . select { |v | v [ 'version' ] . present? }
152
+ . reject { |v | existing_version_numbers . include? ( v [ 'version' ] ) }
153
+ . map do |version |
154
+ {
155
+ number : version [ 'version' ] ,
156
+ published_at : version [ 'published_at' ] ,
157
+ licenses : pkg_metadata [ :licenses ] || 'Unknown' ,
158
+ metadata : {
159
+ 'submodules' => version [ 'submodules' ] ,
160
+ 'providers' => version [ 'providers' ] ,
161
+ }
162
+ }
163
+ end
164
+ end
165
+
166
+ def self . purl_type
167
+ 'terraform'
168
+ end
169
+
170
+ def purl ( package , version = nil )
171
+ # Terraform modules have namespace/name/provider format
172
+ parts = package . name . split ( '/' )
173
+ return nil unless parts . length == 3
174
+
175
+ namespace , name , provider = parts
176
+
177
+ PackageURL . new (
178
+ type : 'terraform' ,
179
+ namespace : "#{ namespace } /#{ provider } " ,
180
+ name : name ,
181
+ version : version . try ( :number )
182
+ ) . to_s
183
+ rescue
184
+ nil
185
+ end
186
+
187
+ private
188
+
189
+ def registry_url_from_parts ( namespace , name , provider )
190
+ "https://registry.terraform.io/modules/#{ namespace } /#{ name } /#{ provider } "
191
+ end
192
+ end
193
+ end
0 commit comments