File Coverage

blib/lib/Net/SecurityCenter/REST.pm
Criterion Covered Total %
statement 151 202 74.7
branch 34 74 45.9
condition 25 77 32.4
subroutine 20 26 76.9
pod 12 13 92.3
total 242 392 61.7


line stmt bran cond sub pod time code
1             package Net::SecurityCenter::REST;
2              
3 2     2   12 use warnings;
  2         3  
  2         52  
4 2     2   8 use strict;
  2         4  
  2         30  
5              
6 2     2   764 use version;
  2         3174  
  2         8  
7 2     2   125 use Carp ();
  2         4  
  2         27  
8 2     2   921 use HTTP::Cookies;
  2         15213  
  2         48  
9 2     2   1080 use JSON;
  2         15636  
  2         10  
10 2     2   1315 use LWP::UserAgent;
  2         42703  
  2         68  
11              
12 2     2   859 use Net::SecurityCenter::Error;
  2         6  
  2         54  
13 2     2   885 use Net::SecurityCenter::Utils qw(trim dumper);
  2         4  
  2         971  
14              
15             our $VERSION = '0.310';
16             our $ERROR;
17              
18             #-------------------------------------------------------------------------------
19             # CONSTRUCTOR
20             #-------------------------------------------------------------------------------
21              
22             sub new {
23              
24 1     1 1 10 my ( $class, $host, $options ) = @_;
25              
26 1 50       5 if ( !$host ) {
27 0         0 Carp::croak 'Specify the Tenable.sc hostname or IP address';
28             }
29              
30 1         35 my $agent = LWP::UserAgent->new();
31 1         2995 my $cookie_jar = HTTP::Cookies->new();
32              
33 1         45 $agent->agent( _agent() );
34 1         79 $agent->ssl_opts( verify_hostname => 0 ); # Disable Host verification
35              
36 1         34 my $timeout = delete( $options->{'timeout'} );
37 1   50     27 my $ssl_opts = delete( $options->{'ssl_options'} ) || {};
38 1   50     14 my $logger = delete( $options->{'logger'} ) || undef;
39 1   50     13 my $scheme = delete( $options->{'scheme'} ) || 'https';
40              
41 1         5 my $url = "$scheme://$host/rest";
42              
43 1 50       4 if ($timeout) {
44 0         0 $agent->timeout($timeout);
45             }
46              
47 1 50       3 if ($ssl_opts) {
48 1         6 $agent->ssl_opts( %{$ssl_opts} );
  1         5  
49             }
50              
51 1         16 $agent->cookie_jar($cookie_jar);
52              
53 1         148 my $self = {
54             host => $host,
55             options => $options,
56             url => $url,
57             token => undef,
58             api_key => undef,
59             agent => $agent,
60             logger => $logger,
61             error => undef,
62             };
63              
64 1         7 bless $self, $class;
65              
66 1 50       9 if ( !$self->_check ) {
67 0         0 Carp::croak $self->{error}->message;
68             }
69              
70 1         9 return $self;
71              
72             }
73              
74             #-------------------------------------------------------------------------------
75             # UTILS
76             #-------------------------------------------------------------------------------
77              
78             sub _agent {
79              
80 1     1   6 my $class = __PACKAGE__;
81 1         14 ( my $agent = $class ) =~ s{::}{-}g;
82              
83 1         37 return "$agent/" . $class->VERSION;
84              
85             }
86              
87             #-------------------------------------------------------------------------------
88              
89             sub _check {
90              
91 1     1   3 my ($self) = @_;
92              
93 1         12 my $response = $self->request( 'GET', '/system' );
94              
95 1 50       4 if ( !$response ) {
96 0         0 $self->error( 'Failed to connect to Tenable.sc (' . $self->{'host'} . ')', 500 );
97 0         0 return;
98             }
99              
100 1         4 $self->{'version'} = $response->{'version'};
101 1         7 $self->{'build_id'} = $response->{'buildID'};
102 1         3 $self->{'license'} = $response->{'licenseStatus'};
103 1         2 $self->{'uuid'} = $response->{'uuid'};
104              
105 1         6 $self->logger( 'info', 'Tenable.sc ' . $self->{'version'} . ' (Build ID:' . $self->{'build_id'} . ')' );
106 1         66 return 1;
107              
108             }
109              
110             #-------------------------------------------------------------------------------
111              
112             sub error {
113              
114 0     0 1 0 my ( $self, $message, $code ) = @_;
115              
116 0 0       0 if ( defined $message ) {
117 0         0 $self->{error} = Net::SecurityCenter::Error->new( $message, $code );
118 0         0 return;
119             } else {
120 0         0 return $self->{error};
121             }
122              
123             }
124              
125             #-------------------------------------------------------------------------------
126             # REST HELPER METHODS (get, head, put, post, delete and patch)
127             #-------------------------------------------------------------------------------
128              
129             for my $sub_name (qw/get head put post delete patch/) {
130              
131             my $req_method = uc $sub_name;
132 2     2   15 no strict 'refs'; ## no critic
  2         3  
  2         2439  
133 0 0 0 0 1 0 eval <<"HERE"; ## no critic
  0 50 0 34 1 0  
  0 0 0 0 0 0  
  0 0 33 0 1 0  
  34 50 66 4 1 127  
  34 0 100 0 1 66  
  34   0     175  
  34   0     167  
  0   0     0  
  0   0     0  
  0   0     0  
  0   0     0  
  0   33     0  
  0   66     0  
  0   100     0  
  0   0     0  
  4   0     13  
  4   0     10  
  4         21  
  4         29  
  0         0  
  0         0  
  0         0  
  0         0  
134             sub $sub_name {
135             my ( \$self, \$path, \$params ) = \@_;
136             my \$class = ref \$self;
137             ( \@_ == 2 || ( \@_ == 3 && ref \$params eq 'HASH' ) )
138             or Carp::croak("Usage: \$class->$sub_name( PATH, [HASHREF] )\n");
139             return \$self->request('$req_method', \$path, \$params || {});
140             }
141             HERE
142              
143             }
144              
145             #-------------------------------------------------------------------------------
146              
147             sub request {
148              
149 44     44 1 113 my ( $self, $method, $path, $params ) = @_;
150              
151 44 50 66     148 ( @_ == 3 || @_ == 4 )
152             or Carp::croak( 'Usage: ' . __PACKAGE__ . '->request(GET|POST|PUT|DELETE|PATCH, $PATH, [\%PARAMS])' );
153              
154 44         86 $method = uc($method);
155 44         156 $path =~ s{^/}{};
156              
157 44 50       208 if ( $method !~ m/(?:GET|POST|PUT|DELETE|PATCH)/ ) {
158 0         0 Carp::carp( $method . ' is an unsupported request method' );
159 0         0 Croak::croak( 'Usage: ' . __PACKAGE__ . '->request(GET|POST|PUT|DELETE|PATCH, $PATH, [\%PARAMS])' );
160             }
161              
162 44         111 my $url = $self->{'url'} . "/$path";
163 44         73 my $agent = $self->{'agent'};
164 44         279 my $request = HTTP::Request->new( $method => $url );
165              
166 44         4906 $self->logger( 'debug', "Method: $method" );
167 44         180 $self->logger( 'debug', "Path: $path" );
168 44         149 $self->logger( 'debug', "URL: $url" );
169              
170             # Don't log credential
171 44 100       149 if ( $path !~ /token/ ) {
172 41         141 $self->logger( 'debug', "Params: " . dumper($params) );
173             }
174              
175 44 50       291 if ( $params->{'file'} ) {
176              
177 0         0 require HTTP::Request::Common;
178              
179             $request = HTTP::Request::Common::POST(
180             $url,
181             'Content-Type' => 'multipart/form-data',
182             'Content' => [
183 0         0 Filedata => [ $params->{'file'}, undef, 'Content-Type' => 'application/octet-stream' ]
184             ],
185             );
186              
187             } else {
188              
189 44         213 $request->header( 'Content-Type', 'application/json' );
190              
191 44 50       2246 if ($params) {
192 44         218 $request->content( encode_json($params) );
193             }
194              
195             }
196              
197             # Reset error
198 44         766 $self->{'error'} = undef;
199              
200 44         132 my $response = $agent->request($request);
201 44         255441 my $response_content = $response->content();
202 44         735 my $response_ctype = $response->headers->{'content-type'};
203 44         243 my $response_code = $response->code();
204              
205 44         348 my $result = {};
206 44         121 my $is_json = ( $response_ctype =~ /application\/json/ );
207              
208             # Force JSON decode for 403 Forbidden message without JSON Content-Type header
209 44 50 33     143 if ( $response_code == 403 && $response_ctype !~ /application\/json/ ) {
210 0         0 $is_json = 1;
211             }
212              
213 44 50       105 if ($is_json) {
214 44         85 $result = eval { decode_json($response_content) };
  44         3028  
215             }
216              
217 44         160 $self->logger( 'debug', 'Response status: ' . $response->status_line );
218              
219 44 100       166 if ( ref $result->{warnings} eq 'ARRAY' ) {
220 41         68 foreach my $warning ( @{ $result->{'warnings'} } ) {
  41         83  
221 0         0 Carp::carp( $warning->{code} . ': ' . $warning->{warning} );
222             }
223             }
224              
225 44 50       132 if ( $response->is_success() ) {
226              
227 44 50       343 if ($is_json) {
228              
229 44 50       90 if ( defined( $result->{'response'} ) ) {
    0          
230 44         517 return $result->{'response'};
231              
232             } elsif ( $result->{'error_msg'} ) {
233              
234 0         0 my $error_msg = trim( $result->{'error_msg'} );
235              
236 0         0 $self->logger( 'error', $error_msg );
237 0         0 $self->error( $error_msg, $response_code );
238              
239 0         0 return;
240              
241             }
242              
243             }
244              
245 0         0 return $response_content;
246              
247             }
248              
249 0 0 0     0 if ( $is_json && exists( $result->{'error_msg'} ) ) {
250              
251 0         0 my $error_msg = trim( $result->{'error_msg'} );
252              
253 0         0 $self->logger( 'error', $error_msg );
254 0         0 $self->error( $error_msg, $response_code );
255              
256 0         0 return;
257              
258             }
259              
260 0         0 $self->logger( 'error', $response_content );
261 0         0 $self->error( $response_content, $response_code );
262              
263 0         0 return;
264              
265             }
266              
267             #-------------------------------------------------------------------------------
268             # HELPER METHODS
269             #-------------------------------------------------------------------------------
270              
271             sub upload {
272              
273 0     0 1 0 my ( $self, $file ) = @_;
274              
275 0 0       0 ( @_ == 2 )
276             or Carp::croak( 'Usage: ' . __PACKAGE__ . '->upload( $FILE )' );
277              
278 0         0 return $self->request( 'POST', '/file/upload', { 'file' => $file } );
279              
280             }
281              
282             #-------------------------------------------------------------------------------
283              
284             sub logger {
285              
286 224     224 1 3194 my ( $self, $level, $message ) = @_;
287              
288 224 50       479 return if ( !$self->{'logger'} );
289              
290 224         342 $level = lc($level);
291              
292 224   50     1352 my $caller = ( caller(1) )[3] || q{};
293 224         1832 $caller =~ s/(::)(\w+)$/->$2/;
294              
295 224         1026 $self->{'logger'}->$level("$caller - $message");
296              
297 224         67562 return 1;
298              
299             }
300              
301             #-------------------------------------------------------------------------------
302              
303             sub login {
304              
305 3     3 1 9 my ( $self, %args ) = @_;
306              
307             # Detect "flat" login argument with username and password
308 3 100 66     36 if ( !( defined( $args{'access_key'} ) && defined( $args{'secret_key'} ) )
      66        
      66        
309             && !( defined( $args{'username'} ) && defined( $args{'password'} ) ) )
310             {
311              
312 1         4 my $username = ( keys %args )[0];
313 1         3 my $password = $args{$username};
314              
315 1         12 %args = (
316             username => $username,
317             password => $password,
318             );
319              
320             }
321              
322 3         8 my $username = delete( $args{'username'} );
323 3         4 my $password = delete( $args{'password'} );
324 3         6 my $access_key = delete( $args{'access_key'} );
325 3         7 my $secret_key = delete( $args{'secret_key'} );
326              
327 3 50 66     8 if ( !$username && !$access_key ) {
328 0         0 Carp::croak('Specify username/password or API Key');
329             }
330              
331 3 100       7 if ($username) {
332              
333 2         9 my $response = $self->request(
334             'POST', '/token',
335             {
336             username => $username,
337             password => $password
338             }
339             );
340              
341 2 50       8 return if ( !$response );
342              
343 2         4 $self->{'token'} = $response->{'token'};
344 2         10 $self->{'agent'}->default_header( 'X-SecurityCenter', $self->{'token'} );
345              
346 2         113 $self->logger( 'info', 'Connected to Tenable.sc (' . $self->{'host'} . ')' );
347 2         7 $self->logger( 'debug', "User: $username" );
348              
349             }
350              
351 3 100       10 if ($access_key) {
352              
353 1         24 my $version_check = ( version->parse( $self->{'version'} ) <=> version->parse('5.13.0') );
354              
355 1 50       7 if ( $version_check < 0 ) {
356 0         0 Carp::croak "API Key Authentication require Tenable.sc v5.13.0 or never";
357             }
358              
359 1         2 $self->{'api_key'} = 1;
360 1         7 $self->{'agent'}->default_header( 'X-APIKey', "accessKey=$access_key; secretKey=$secret_key" );
361              
362 1         59 my $response = $self->request( 'GET', '/currentUser' );
363              
364 1 50       3 return if ( !$response );
365              
366 1         6 $self->logger( 'info', 'Connected to Tenable.sc (' . $self->{'host'} . ') using API Key' );
367              
368             }
369              
370 3         12 return 1;
371              
372             }
373              
374             #-------------------------------------------------------------------------------
375              
376             sub logout {
377              
378 1     1 1 3 my ($self) = @_;
379              
380 1 50       5 if ( $self->{'token'} ) {
381 1         4 $self->request( 'DELETE', '/token' );
382 1         3 $self->{'token'} = undef;
383             }
384              
385 1 50       5 if ( $self->{'api_key'} ) {
386 1         5 $self->{'agent'}->default_header( 'X-APIKey', undef );
387 1         48 $self->{'api_key'} = undef;
388             }
389              
390 1         6 $self->logger( 'info', 'Disconnected from Tenable.sc (' . $self->{'host'} . ')' );
391              
392 1         5 return 1;
393              
394             }
395              
396             #-------------------------------------------------------------------------------
397              
398             sub DESTROY {
399              
400 1     1   671 my ($self) = @_;
401              
402 1 50       5 if ( $self->{'token'} ) {
403 0         0 $self->logout();
404             }
405              
406 1         44 return;
407              
408             }
409              
410             #-------------------------------------------------------------------------------
411              
412             1;
413              
414             __END__