passivetotal 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ba999440914765150f5879f7e1477360c8004395
4
- data.tar.gz: 4c5af19bed80397c721e95908500993825d15675
3
+ metadata.gz: d1844555714450cff7c7fdf042d3bd9888e300eb
4
+ data.tar.gz: a1b743c9a386b12981e533cbbcdfcd60568bcede
5
5
  SHA512:
6
- metadata.gz: 3ab4e9de47859d9a39ff2529701fdeb7bd71f625dfa62643a2ab043bca552c0e51a796401b008f7a1024a8f1f6acbe1aaec1e9c7c27e6f67338f5b6898130289
7
- data.tar.gz: e7b9320ce2cd7638ee222220ed314de0c7044c5675273bc5be2b6e2fed643f568c36a324161a302cf3250b85f5d01207c95777275ba2095c57e79c18a4761a69
6
+ metadata.gz: b154abc61be44ad6be8b96afcdaff311022d18722f26192d6cb498a31f872be038c7ffec72a5a8b1255ca378d30271b32419ea89282a3c514e9dc47c89cb92ba
7
+ data.tar.gz: 51432b063381d9169e6694577ff42eec84b7f58ca5dddfebcb767fdb4958fa715663193e7e162cfd477f3cc31f7f53a9981c113b29ddee9fcb0b157c448b294d
data/.gitignore CHANGED
@@ -7,3 +7,5 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ .DS_Store
11
+ rdoc
data/README.md CHANGED
@@ -22,9 +22,10 @@ Or install it yourself as:
22
22
 
23
23
  Included in the gem is a command-line tool, passivetotal, with the following usage:
24
24
 
25
- Usage: passivetotal [-h] [-v] [-k <apikey>] [[-m|-p|-c|-t|-e|-w] <ip or dom>] [[-s|-d] <dom>] [-x <ip>] [-l <ip or hash>] [-i <value>]
25
+ Usage: bin/passivetotal [-v] [-u <username>] [-k <apikey>] <action flag> <query> [-i <value>]
26
26
  -h Help
27
27
  -v Verbose output
28
+ -u <username> Sets the Username, defaults to the environment variable PASSIVETOTAL_USERNAME
28
29
  -k <apikey> Sets the APIKEY, defaults to the environment variable PASSIVETOTAL_APIKEY
29
30
  ACTIONS (You have to select one, last one wins) -m <ip or dom> Queries metadata for given IP or domain
30
31
  -p <ip or dom> Queries passive DNS data for given IP or domain
@@ -36,35 +37,37 @@ Included in the gem is a command-line tool, passivetotal, with the following usa
36
37
  -s <dom> Queries the subdomains for a given domain
37
38
  -d <dom> Queries (or sets) if a domain is a dynamic DNS domain
38
39
  -x <ip> Queries (or sets) if a given IP is a sinkhole
39
- -l <ip or hash> Queries for SSL Certificates/IP addresses associated with a given IP or SHA-1 hash
40
+ -l <hash> Queries for SSL certificates/IP addresses associated with a given SHA-1 hash
41
+ -H <ip or hash> Queries for SSL certificate history associated with a given IP or SHA-1 hash
42
+ -T <ip or dom> Queries for Tracker information associated with a given IP or domain
43
+ -o <ip or dom> Queries for OSINT on a given IP or domain
40
44
  SETTING VALUES -i <value> Sets the value, used in conjuntion with -c, -t, -e, -w, -d, or -x
41
45
  Valid values for -i depend on what it's used with:
42
- -c : targeted, crime, multiple, benign
46
+ -c : malicious, non-malicious, suspicious, unknown
43
47
  -t : <a tag name consisting of characters: [a-zA-Z_]>
44
48
  -e, -w, -d, -x: true, false
45
-
46
49
  ## Usage
47
50
 
48
- # Initialize the API wrapper with an apikey (using the default endpoint URL of https://www.passivetotal.org/api/v1/)
49
- pt = PassiveTotal::API.new(apikey)
51
+ # Initialize the API wrapper with an apikey (using the default endpoint URL of https://api.passivetotal.org/v2/)
52
+ pt = PassiveTotal::API.new(user, apikey)
50
53
  # Create an array to shove results into
51
- res = []
52
- # query metadata for the domain, www.passivetotal.org
53
- res << @pt.metadata('www.passivetotal.org')
54
- # query metadata for the ipv4 address, 107.170.89.121
55
- res << @pt.metadata('107.170.89.121')
54
+ res = Array.new
55
+ # query enrichment for the domain, www.passivetotal.org
56
+ res << @pt.enrichment('www.passivetotal.org')
57
+ # query enrichment for the ipv4 address, 107.170.89.121
58
+ res << @pt.enrichment('107.170.89.121')
56
59
  # query passive DNS results for the domain, www.passivetotal.org
57
60
  res << @pt.passive('www.passivetotal.org')
58
61
  # query passive DNS results for the ipv4 address, 107.170.89.121
59
62
  res << @pt.passive('107.170.89.121')
60
63
  # query for subdomains of passivetotal.org
61
- res << @pt.subdomains('passivetotal.org')
64
+ #res << @pt.subdomains('passivetotal.org')
62
65
  # query for unique IPv4 resolutions of passivetotal.org
63
66
  res << @pt.unique('passivetotal.org')
64
67
  # query for the classification of www.passivetotal.org
65
68
  res << @pt.classification('www.passivetotal.org')
66
69
  # set the classification of www.passivetotal.org as benign
67
- res << @pt.classification('www.passivetotal.org', 'benign')
70
+ res << @pt.classification('www.passivetotal.org', 'non-malicious')
68
71
  # query for the tags associated with www.chrisleephd.us
69
72
  res << @pt.tags('www.chrisleephd.us')
70
73
  # add the "cool" tag to www.chrisleephd.us
@@ -84,13 +87,17 @@ Included in the gem is a command-line tool, passivetotal, with the following usa
84
87
  # flag www.passivetotal.org as not a dynamic dns domain/host
85
88
  res << @pt.dynamic('www.passivetotal.org', false)
86
89
  # check if www.passivetotal.org is being watched
87
- res << @pt.watching('www.passivetotal.org')
90
+ res << @pt.monitor('www.passivetotal.org')
88
91
  # unwatch www.passivetotal.org
89
- res << @pt.watching('www.passivetotal.org', false)
90
- # list SSL certificates associated with IPV4 address 104.131.121.205
91
- res << @pt.ssl_certificate('104.131.121.205')
92
+ res << @pt.monitor('www.passivetotal.org', false)
92
93
  # list sites associated with SSL certificates with SHA-1 hash of e9a6647d6aba52dc47b3838c920c9ee59bad7034
93
94
  res << @pt.ssl_certificate('e9a6647d6aba52dc47b3838c920c9ee59bad7034')
95
+ # list sites associated with SSL certificates with SHA-1 hash of e9a6647d6aba52dc47b3838c920c9ee59bad7034
96
+ res << @pt.ssl_certificate('2317683628587350290823564500811277499', 'serialNumber')
97
+ # retrieve certificate history based on SHA-1 hash of e9a6647d6aba52dc47b3838c920c9ee59bad7034
98
+ res << @pt.ssl_certificate_history('e9a6647d6aba52dc47b3838c920c9ee59bad7034')
99
+ # retrieve certificate history from IPv4 address of 52.8.228.23
100
+ res << @pt.ssl_certificate_history('52.8.228.23')
94
101
  # dump all this glorious information to feast your eyes upon
95
102
  pp res
96
103
 
@@ -9,10 +9,12 @@ require 'passivetotal/version'
9
9
  module PassiveTotal # :nodoc:
10
10
 
11
11
  class InvalidAPIKeyError < ArgumentError; end
12
+ class APIUsageError < StandardError; end
13
+ class ExceededQuotaError < StandardError; end
12
14
 
13
15
  class Transaction < Struct.new(:query, :response, :response_time); end
14
16
  class Query < Struct.new(:api, :query, :set, :url, :parameters); end
15
- class Response < Struct.new(:json, :success, :request_time, :raw_query, :error, :result_count, :results); end
17
+ class Response < Struct.new(:json, :success, :results); end
16
18
 
17
19
  # The API class wraps the PassiveTotal.org web API for all the verbs that it supports
18
20
  # See https://www.passivetotal.org/api/docs for the API documentation.
@@ -23,167 +25,309 @@ module PassiveTotal # :nodoc:
23
25
  TLDS = "abb,abbott,abogado,ac,academy,accenture,accountant,accountants,active,actor,ad,ads,adult,ae,aeg,aero,af,afl,ag,agency,ai,aig,airforce,al,allfinanz,alsace,am,amsterdam,an,android,ao,apartments,aq,aquarelle,ar,archi,army,arpa,as,asia,associates,at,attorney,au,auction,audio,auto,autos,aw,ax,axa,az,azure,ba,band,bank,bar,barclaycard,barclays,bargains,bauhaus,bayern,bb,bbc,bbva,bd,be,beer,berlin,best,bf,bg,bh,bharti,bi,bible,bid,bike,bing,bingo,bio,biz,bj,black,blackfriday,bloomberg,blue,bm,bmw,bn,bnl,bnpparibas,bo,boats,bond,boo,boutique,br,bradesco,bridgestone,broker,brother,brussels,bs,bt,budapest,build,builders,business,buzz,bv,bw,by,bz,bzh,ca,cab,cafe,cal,camera,camp,cancerresearch,canon,capetown,capital,caravan,cards,care,career,careers,cars,cartier,casa,cash,casino,cat,catering,cba,cbn,cc,cd,center,ceo,cern,cf,cfa,cfd,cg,ch,channel,chat,cheap,chloe,christmas,chrome,church,ci,cisco,citic,city,ck,cl,claims,cleaning,click,clinic,clothing,cloud,club,cm,cn,co,coach,codes,coffee,college,cologne,com,commbank,community,company,computer,condos,construction,consulting,contractors,cooking,cool,coop,corsica,country,coupons,courses,cr,credit,creditcard,cricket,crown,crs,cruises,cu,cuisinella,cv,cw,cx,cy,cymru,cyou,cz,dabur,dad,dance,date,dating,datsun,day,dclk,de,deals,degree,delivery,democrat,dental,dentist,desi,design,dev,diamonds,diet,digital,direct,directory,discount,dj,dk,dm,dnp,do,docs,dog,doha,domains,doosan,download,drive,durban,dvag,dz,earth,eat,ec,edu,education,ee,eg,email,emerck,energy,engineer,engineering,enterprises,epson,equipment,er,erni,es,esq,estate,et,eu,eurovision,eus,events,everbank,exchange,expert,exposed,express,fail,faith,fan,fans,farm,fashion,feedback,fi,film,finance,financial,firmdale,fish,fishing,fit,fitness,fj,fk,flights,florist,flowers,flsmidth,fly,fm,fo,foo,football,forex,forsale,foundation,fr,frl,frogans,fund,furniture,futbol,fyi,ga,gal,gallery,garden,gb,gbiz,gd,gdn,ge,gent,genting,gf,gg,ggee,gh,gi,gift,gifts,gives,gl,glass,gle,global,globo,gm,gmail,gmo,gmx,gn,gold,goldpoint,golf,goo,goog,google,gop,gov,gp,gq,gr,graphics,gratis,green,gripe,gs,gt,gu,guge,guide,guitars,guru,gw,gy,hamburg,hangout,haus,healthcare,help,here,hermes,hiphop,hitachi,hiv,hk,hm,hn,hockey,holdings,holiday,homedepot,homes,honda,horse,host,hosting,hoteles,hotmail,house,how,hr,ht,hu,ibm,icbc,icu,id,ie,ifm,il,im,immo,immobilien,in,industries,infiniti,info,ing,ink,institute,insure,int,international,investments,io,iq,ir,irish,is,it,iwc,java,jcb,je,jetzt,jewelry,jlc,jll,jm,jo,jobs,joburg,jp,juegos,kaufen,kddi,ke,kg,kh,ki,kim,kitchen,kiwi,km,kn,koeln,komatsu,kp,kr,krd,kred,kw,ky,kyoto,kz,la,lacaixa,land,lasalle,lat,latrobe,law,lawyer,lb,lc,lds,lease,leclerc,legal,lgbt,li,liaison,lidl,life,lighting,limited,limo,link,lk,loan,loans,lol,london,lotte,lotto,love,lr,ls,lt,ltda,lu,lupin,luxe,luxury,lv,ly,ma,madrid,maif,maison,management,mango,market,marketing,markets,marriott,mba,mc,md,me,media,meet,melbourne,meme,memorial,men,menu,mg,mh,miami,microsoft,mil,mini,mk,ml,mm,mma,mn,mo,mobi,moda,moe,monash,money,montblanc,mormon,mortgage,moscow,motorcycles,mov,movie,movistar,mp,mq,mr,ms,mt,mtn,mtpc,mu,museum,mv,mw,mx,my,mz,na,nadex,nagoya,name,navy,nc,ne,nec,net,netbank,network,neustar,new,news,nexus,nf,ng,ngo,nhk,ni,nico,ninja,nissan,nl,no,np,nr,nra,nrw,ntt,nu,nyc,nz,office,okinawa,om,omega,one,ong,onl,online,ooo,oracle,org,organic,osaka,otsuka,ovh,pa,page,panerai,paris,partners,parts,party,pe,pf,pg,ph,pharmacy,philips,photo,photography,photos,physio,piaget,pics,pictet,pictures,pink,pizza,pk,pl,place,play,plumbing,plus,pm,pn,pohl,poker,porn,post,pr,praxi,press,pro,prod,productions,prof,properties,property,ps,pt,pub,pw,py,qa,qpon,quebec,racing,re,realtor,recipes,red,redstone,rehab,reise,reisen,reit,ren,rent,rentals,repair,report,republican,rest,restaurant,review,reviews,rich,ricoh,rio,rip,ro,rocks,rodeo,rs,rsvp,ru,ruhr,run,rw,ryukyu,sa,saarland,sale,samsung,sandvik,sandvikcoromant,sap,sarl,saxo,sb,sc,sca,scb,schmidt,scholarships,school,schule,schwarz,science,scor,scot,sd,se,seat,sener,services,sew,sex,sexy,sg,sh,shiksha,shoes,show,shriram,si,singles,site,sj,sk,ski,sky,skype,sl,sm,sn,sncf,so,soccer,social,software,sohu,solar,solutions,sony,soy,space,spiegel,spreadbetting,sr,st,starhub,statoil,study,style,su,sucks,supplies,supply,support,surf,surgery,suzuki,sv,swatch,swiss,sx,sy,sydney,systems,sz,taipei,tatar,tattoo,tax,taxi,tc,td,team,tech,technology,tel,telefonica,temasek,tennis,tf,tg,th,thd,theater,tickets,tienda,tips,tires,tirol,tj,tk,tl,tm,tn,to,today,tokyo,tools,top,toray,toshiba,tours,town,toys,tr,trade,trading,training,travel,trust,tt,tui,tv,tw,tz,ua,ug,uk,university,uno,uol,us,uy,uz,va,vacations,vc,ve,vegas,ventures,versicherung,vet,vg,vi,viajes,video,villas,vision,vista,vistaprint,vlaanderen,vn,vodka,vote,voting,voto,voyage,vu,wales,walter,wang,watch,webcam,website,wed,wedding,weir,wf,whoswho,wien,wiki,williamhill,win,windows,wme,work,works,world,ws,wtc,wtf,xbox,xerox,xin,xn--1qqw23a,xn--30rr7y,xn--3bst00m,xn--3ds443g,xn--3e0b707e,xn--45brj9c,xn--45q11c,xn--4gbrim,xn--55qw42g,xn--55qx5d,xn--6frz82g,xn--6qq986b3xl,xn--80adxhks,xn--80ao21a,xn--80asehdb,xn--80aswg,xn--90a3ac,xn--90ais,xn--9et52u,xn--b4w605ferd,xn--c1avg,xn--cg4bki,xn--clchc0ea0b2g2a9gcd,xn--czr694b,xn--czrs0t,xn--czru2d,xn--d1acj3b,xn--d1alf,xn--estv75g,xn--fiq228c5hs,xn--fiq64b,xn--fiqs8s,xn--fiqz9s,xn--fjq720a,xn--flw351e,xn--fpcrj9c3d,xn--fzc2c9e2c,xn--gecrj9c,xn--h2brj9c,xn--hxt814e,xn--i1b6b1a6a2e,xn--imr513n,xn--io0a7i,xn--j1amh,xn--j6w193g,xn--kcrx77d1x4a,xn--kprw13d,xn--kpry57d,xn--kput3i,xn--l1acc,xn--lgbbat1ad8j,xn--mgb9awbf,xn--mgba3a4f16a,xn--mgbaam7a8h,xn--mgbab2bd,xn--mgbayh7gpa,xn--mgbbh1a71e,xn--mgbc0a9azcg,xn--mgberp4a5d4ar,xn--mgbpl2fh,xn--mgbx4cd0ab,xn--mxtq1m,xn--ngbc5azd,xn--node,xn--nqv7f,xn--nqv7fs00ema,xn--nyqy26a,xn--o3cw4h,xn--ogbpf8fl,xn--p1acf,xn--p1ai,xn--pgbs0dh,xn--q9jyb4c,xn--qcka1pmc,xn--rhqv96g,xn--s9brj9c,xn--ses554g,xn--unup4y,xn--vermgensberater-ctb,xn--vermgensberatung-pwb,xn--vhquv,xn--vuq861b,xn--wgbh1c,xn--wgbl6a,xn--xhq521b,xn--xkc2al3hye2a,xn--xkc2dl3a5ee0h,xn--y9a3aq,xn--yfro4i67o,xn--ygbi2ammx,xn--zfr164b,xxx,xyz,yachts,yandex,ye,yodobashi,yoga,yokohama,youtube,yt,za,zip,zm,zone,zuerich,zw".split(/,/)
24
26
 
25
27
  # initialize a new PassiveTotal::API object
28
+ # username: the email address associated with your PassiveTotal API key.
26
29
  # apikey: is 64-hexcharacter string
27
- # endpoint: base URL for the web service, defaults to https://www.passivetotal.org/api/v1/
28
- def initialize(apikey, endpoint = 'https://www.passivetotal.org/api/v1/')
30
+ # endpoint: base URL for the web service, defaults to https://api.passivetotal.org/v2/
31
+ def initialize(username, apikey, endpoint = 'https://api.passivetotal.org/v2/')
29
32
  unless apikey =~ /^[a-fA-F0-9]{64}$/
30
33
  raise ArgumentError.new("apikey must be a 64 character hex string")
31
34
  end
35
+ @username = username
32
36
  @apikey = apikey
33
37
  @endpoint = endpoint
34
38
  end
35
39
 
36
- # Metadata describes the item being queried and includes many of the options available inside of the action API calls.
40
+ # Account : Get account details your account.
41
+ def account
42
+ get('account')
43
+ end
44
+
45
+ # Account History : Get history associated with your account.
46
+ def account_history
47
+ get('account/history')
48
+ end
49
+
50
+ # history is an alias for account_history
51
+ alias_method :history, :account_history
52
+
53
+ # Account notifications : Get notifications that have been posted to your account.
54
+ def account_notifications
55
+ get('account/notifications')
56
+ end
57
+
58
+ # notifications is an alias for account_notifications
59
+ alias_method :notifications, :account_notifications
60
+
61
+ # Account organization : Get details about the organization your account is associated with.
62
+ def account_organization
63
+ get('account/organization')
64
+ end
65
+
66
+ # organization is an alias for account_organization
67
+ alias_method :organization, :account_organization
68
+
69
+ # Account organization teamstream : Get the teamstream for the organization your account is associated with.
70
+ def account_organization_teamstream
71
+ get('account/organization/teamstream')
72
+ end
73
+
74
+ # teamstream is an alias for account_organization_teamstream
75
+ alias_method :teamstream, :account_organization_teamstream
76
+
77
+ # Account sources : Get source details for a specific source.
78
+ def account_sources(source)
79
+ get('account/sources', {'source' => source})
80
+ end
81
+
82
+ # sources is an alias for account_sources
83
+ alias_method :sources, :account_sources
84
+
85
+
86
+ # Passive provides a complete passive DNS picture for a domain or IP address including first/last seen values, deconflicted values, sources used, unique counts and enrichment for all values.
37
87
  # query: A domain or IP address to query
38
- def metadata(query)
88
+ def passive(query)
39
89
  is_valid_with_error(__method__, [:ipv4, :domain], query)
40
90
  if domain?(query)
41
91
  query = normalize_domain(query)
42
92
  end
43
- get(__method__, query)
93
+ get('dns/passive', {'query' => query})
44
94
  end
45
-
95
+
46
96
  # Passive provides a complete passive DNS picture for a domain or IP address including first/last seen values, deconflicted values, sources used, unique counts and enrichment for all values.
47
97
  # query: A domain or IP address to query
48
- def passive(query)
98
+ def passive_unique(query)
49
99
  is_valid_with_error(__method__, [:ipv4, :domain], query)
50
100
  if domain?(query)
51
101
  query = normalize_domain(query)
52
102
  end
53
- get(__method__, query)
54
- end
55
-
56
- # Subdomains provides a comprehensive view of all known subdomains for a registered domain with associated passive DNS information. This call is best used to understand the activity of a particular domain over a period of time. Passive DNS information is only deconflicted at the subdomain level, not across the entire domain.
57
- # query: A domain to query
58
- def subdomains(query)
59
- is_valid_with_error(__method__, [:domain], query)
60
- query = normalize_domain(query)
61
- get(__method__, query)
103
+ get('dns/passive/unique', {'query' => query})
62
104
  end
63
-
64
- # Each domain or IP address with results has a unique set of resolving items. This call provides those unique items and a frequency count of how often they show up in sorted order.
105
+
106
+ # unique is an alias for passive_unique
107
+ alias_method :unique, :passive_unique
108
+
109
+ # Enrichment : Enrich the given query with metadata
65
110
  # query: A domain or IP address to query
66
- def unique(query)
111
+ def enrichment(query)
67
112
  is_valid_with_error(__method__, [:ipv4, :domain], query)
68
113
  if domain?(query)
69
114
  query = normalize_domain(query)
70
115
  end
71
- get(__method__, query)
116
+ get('enrichment', {'query' => query})
72
117
  end
73
118
 
74
- # PassiveTotal uses the notion of classifications to highlight table rows a certain color based on how they have been rated.
75
- # PassiveTotal::API#classification() queries if only one argument is given, and sets if both are given
119
+ # metadata is an alias for enrichment
120
+ alias_method :metadata, :enrichment
121
+
122
+ # osint: Get opensource intelligence data
76
123
  # query: A domain or IP address to query
77
- # set: classification label, one of [targeted, crime, multiple, benign]
78
- def classification(query, set=nil)
124
+ def osint(query)
79
125
  is_valid_with_error(__method__, [:ipv4, :domain], query)
80
126
  if domain?(query)
81
127
  query = normalize_domain(query)
82
128
  end
83
- if set.nil?
84
- get(__method__, query)
129
+ get('enrichment/osint', {'query' => query})
130
+ end
131
+
132
+ # subdomains: Get subdomains using a wildcard query
133
+ # query: A domain with wildcard, e.g., *.passivetotal.org
134
+ def subdomains(query)
135
+ get('enrichment/subdomains', {'query' => query})
136
+ end
137
+
138
+ # whois: Get WHOIS data for a domain or IP address
139
+ # query: ipv4, domain, or, if you specify a field, any value for that field
140
+ # field: field name to query if not the default ip/domain field
141
+ # field names: domain, email, name, organization, address, phone, nameserver
142
+ def whois(query, field=nil)
143
+ if field
144
+ is_valid_with_error(__method__, [:whois_field], field)
145
+ get('whois/search', {'field' => field, 'query' => query})
85
146
  else
86
- is_valid_with_error(__method__, [:classification], set)
87
- post(__method__, query, set)
147
+ is_valid_with_error(__method__, [:ipv4, :domain], query)
148
+ if domain?(query)
149
+ query = normalize_domain(query)
150
+ end
151
+ get('whois', {'query' => query, 'compact_record' => 'false'})
88
152
  end
89
153
  end
154
+
155
+ # Add a user-tag to an IP or domain
156
+ # query: A domain or IP address to tag
157
+ # tag: Value used to tag query value. Should only consist of alphanumeric, underscores and hyphen values
158
+ def add_tag(query, tag)
159
+ is_valid_with_error(__method__, [:ipv4, :domain], query)
160
+ is_valid_with_error(__method__, [:tag], tag)
161
+ post('actions/tags', { 'query' => query, 'tags' => [tag] })
162
+ end
90
163
 
91
- # PassiveTotal allows users to notate if an IP address is a known sinkhole. These values are shared globally with everyone in the platform.
92
- # PassiveTotal::API#sinkhole() queries if only one argument is given, and sets if both are given
93
- # query: An IP address to set as a sinkhole or not
94
- # set: String-boolean of "true" or "false"
95
- def sinkhole(query, set=nil)
96
- is_valid_with_error(__method__, [:ipv4], query)
164
+ # Remove a user-tag to an IP or domain
165
+ # query: A domain or IP address to remove a tag from
166
+ # tag: Value used to tag query value. Should only consist of alphanumeric, underscores and hyphen values
167
+ def remove_tag(query, tag)
168
+ is_valid_with_error(__method__, [:ipv4, :domain], query)
169
+ is_valid_with_error(__method__, [:tag], tag)
170
+ delete('actions/tags', { 'query' => query, 'tags' => [tag] })
171
+ end
172
+
173
+ # PassiveTotal uses the notion of classifications to highlight table rows a certain color based on how they have been rated.
174
+ # PassiveTotal::API#classification() queries if only one argument is given, and sets if both are given
175
+ # query: A domain or IP address to query
176
+ def classification(query, set=nil)
177
+ is_valid_with_error(__method__, [:ipv4, :domain], query)
178
+ if domain?(query)
179
+ query = normalize_domain(query)
180
+ end
97
181
  if set.nil?
98
- get(__method__, query)
182
+ get('actions/classification', {'query' => query})
99
183
  else
100
- is_valid_with_error(__method__, [:bool], set)
101
- post(__method__, query, set)
184
+ is_valid_with_error(__method__.to_s, [:classification], set)
185
+ post('actions/classification', { 'query' => query, 'classification' => set })
102
186
  end
103
187
  end
104
188
 
105
189
  # PassiveTotal allows users to notate if a domain or IP address have ever been compromised. These values aid in letting users know that a site may be benign, but it was used in an attack at some point in time.
106
190
  # PassiveTotal::API#ever_compromised() queries if only one argument is given, and sets if both are given
107
191
  # query: A domain or IP address to query
108
- # set: String-boolean of "true" or "false"
192
+ # set: a boolean flag
109
193
  def ever_compromised(query, set=nil)
110
194
  is_valid_with_error(__method__, [:ipv4, :domain], query)
111
195
  if domain?(query)
112
196
  query = normalize_domain(query)
113
197
  end
114
198
  if set.nil?
115
- get(__method__, query)
199
+ get('actions/ever-compromised', {'query' => query})
116
200
  else
117
201
  is_valid_with_error(__method__, [:bool], set)
118
- post(__method__, query, set)
202
+ post('actions/ever-compromised', { 'query' => query, 'status' => set })
119
203
  end
120
204
  end
121
205
 
206
+ alias_method :compromised, :ever_compromised
207
+
122
208
  # PassiveTotal allows users to notate if a domain is associated with a dynamic DNS provider.
123
209
  # PassiveTotal::API#dynamic() queries if only one argument is given, and sets if both are given
124
210
  # query: A domain to query
125
- # set: String-boolean of "true" or "false"
211
+ # set: a boolean flag
126
212
  def dynamic(query, set=nil)
127
213
  is_valid_with_error(__method__, [:domain], query)
128
214
  query = normalize_domain(query)
129
215
  if set.nil?
130
- get(__method__, query)
216
+ get('actions/dynamic-dns', {'query' => query})
131
217
  else
132
218
  is_valid_with_error(__method__, [:bool], set)
133
- post(__method__, query, set)
219
+ post('actions/dynamic-dns', { 'query' => query, 'status' => set })
134
220
  end
135
221
  end
136
222
 
137
- # PassiveTotal allows users to "watch" domains or IP addresses in order to get notified of any changes.
138
- # PassiveTotal::API#watching() queries if only one argument is given, and sets if both are given
139
- # query: A domain or IP address to query
140
- # set: String-boolean of "true" or "false"
141
- def watching(query, set=nil)
223
+ # PassiveTotal allows users to notate if an ip or domain is "monitored".
224
+ # PassiveTotal::API#monitor() queries if only one argument is given, and sets if both are given
225
+ # query: A domain to query
226
+ # set: a boolean flag
227
+ def monitor(query, set=nil)
142
228
  is_valid_with_error(__method__, [:ipv4, :domain], query)
143
229
  if domain?(query)
144
230
  query = normalize_domain(query)
145
231
  end
146
232
  if set.nil?
147
- get(__method__, query)
233
+ get('actions/monitor', {'query' => query})
234
+ else
235
+ is_valid_with_error(__method__, [:bool], set)
236
+ post('actions/monitor', { 'query' => query, 'status' => set })
237
+ end
238
+ end
239
+
240
+ # monitoring is an alias for monitor
241
+ alias_method :monitoring, :monitor
242
+ alias_method :watching, :monitor
243
+
244
+ # PassiveTotal allows users to notate if an IP address is a known sinkhole. These values are shared globally with everyone in the platform.
245
+ # PassiveTotal::API#sinkhole() queries if only one argument is given, and sets if both are given
246
+ # query: An IP address to set as a sinkhole or not
247
+ # set: a boolean flag
248
+ def sinkhole(query, set=nil)
249
+ is_valid_with_error(__method__, [:ipv4], query)
250
+ if set.nil?
251
+ get('actions/sinkhole', {'query' => query})
148
252
  else
149
253
  is_valid_with_error(__method__, [:bool], set)
150
- post(__method__, query, set)
254
+ post('actions/sinkhole', { 'query' => query, 'status' => set })
255
+ end
256
+ end
257
+
258
+
259
+ # PassiveTotal uses three types of tags (user, global, and temporal) in order to provide context back to the user.
260
+ # query: A domain or IP address to query
261
+ # set: if supplied, adds a tag to an entity
262
+ def tags(query, set=nil)
263
+ is_valid_with_error(__method__, [:ipv4, :domain], query)
264
+ if domain?(query)
265
+ query = normalize_domain(query)
266
+ end
267
+ if set.nil?
268
+ get('actions/tags', {'query' => query})
269
+ else
270
+ is_valid_with_error(__method__, [:tag], set)
271
+ post('actions/tag', { 'query' => query, 'tags' => [set] })
151
272
  end
152
273
  end
153
274
 
275
+ # Search Tags : Search for items based on tag value
154
276
  # PassiveTotal uses three types of tags (user, global, and temporal) in order to provide context back to the user.
155
277
  # query: A domain or IP address to query
156
- def tags(query)
278
+ def tags_search(query)
157
279
  is_valid_with_error(__method__, [:ipv4, :domain], query)
158
- get("user/tags", query)
280
+ if domain?(query)
281
+ query = normalize_domain(query)
282
+ end
283
+ get('actions/tags/search', {'query' => query})
159
284
  end
160
285
 
161
- # Add a user-tag to an IP or domain
162
- # query: A domain or IP address to tag
163
- # tag: Value used to tag query value. Should only consist of alphanumeric, underscores and hyphen values
164
- def add_tag(query, tag)
165
- is_valid_with_error(__method__, [:ipv4, :domain], query)
166
- is_valid_with_error(__method__, [:tag], tag)
167
- post_tag("user/tag/add", query, tag)
286
+ # PassiveTotal collects and provides SSL certificates as an enrichment point when possible. Beyond the certificate data itself, PassiveTotal keeps a record of the IP address of where the certificate was found and the time in which it was collected.
287
+ # query: A SHA-1 hash to query
288
+ def ssl_certificate_history(query)
289
+ is_valid_with_error(__method__, [:ipv4, :hash], query)
290
+ get('ssl-certificate/history', {'query' => query})
291
+ end
292
+
293
+ # ssl_certificate: returns details about SSL certificates
294
+ # query: SHA-1 has to query, or, if field is set, a valid value for that field
295
+ # field: the certificate field to query upon
296
+ # certificate fields: issuer_surname, subject_organizationName, issuer_country, issuer_organizationUnitName, fingerprint, subject_organizationUnitName, serialNumber, subject_emailAddress, subject_country, issuer_givenName, subject_commonName, issuer_commonName, issuer_stateOrProvinceName, issuer_province, subject_stateOrProvinceName, sha1, sslVersion, subject_streetAddress, subject_serialNumber, issuer_organizationName, subject_surname, subject_localityName, issuer_streetAddress, issuer_localityName, subject_givenName, subject_province, issuer_serialNumber, issuer_emailAddress
297
+ def ssl_certificate(query, field=nil)
298
+ if field.nil?
299
+ is_valid_with_error(__method__, [:hash], query)
300
+ get('ssl-certificate', {'query' => query})
301
+ else
302
+ is_valid_with_error(__method__, [:ssl_field], field)
303
+ get_params('ssl-certificate/search', { 'query' => query, 'field' => field })
304
+ end
168
305
  end
169
306
 
170
- # Remove a user-tag to an IP or domain
171
- # query: A domain or IP address to remove a tag from
172
- # tag: Value used to tag query value. Should only consist of alphanumeric, underscores and hyphen values
173
- def remove_tag(query, tag)
307
+ # PassiveTotal tracks some interesting metadata about a host
308
+ # query: a hostname or ip address
309
+ def components(query)
174
310
  is_valid_with_error(__method__, [:ipv4, :domain], query)
175
- is_valid_with_error(__method__, [:tag], tag)
176
- post_tag("user/tag/remove", query, tag)
311
+ if domain?(query)
312
+ query = normalize_domain(query)
313
+ end
314
+ get('host-attributes/components', {'query' => query})
177
315
  end
178
316
 
179
- # PassiveTotal collects and provides SSL certificates as an enrichment point when possible. Beyond the certificate data itself, PassiveTotal keeps a record of the IP address of where the certificate was found and the time in which it was collected.
180
- # query: An IP address or SHA-1 hash to query
181
- def ssl_certificate(query)
182
- is_valid_with_error(__method__, [:ipv4, :hash], query)
183
- if ipv4?(query)
184
- get("ssl_certificate/ip_address", query)
185
- elsif hash?(query)
186
- get("ssl_certificate/hash", query)
317
+ # trackers: Get all tracking codes for a domain or IP address.
318
+ # query: ip or domain, or, if type is supplied, a valid tracker ID
319
+ # type: A valid tracker type to search:
320
+ # tracker types: YandexMetricaCounterId, ClickyId, GoogleAnalyticsAccountNumber, NewRelicId, MixpanelId, GoogleAnalyticsTrackingId
321
+ def trackers(query, type=nil)
322
+ if type.nil?
323
+ is_valid_with_error(__method__, [:ipv4, :domain], query)
324
+ if domain?(query)
325
+ query = normalize_domain(query)
326
+ end
327
+ get('host-attributes/trackers', {'query' => query})
328
+ else
329
+ is_valid_with_error(__method__, [:tracker_type], type)
330
+ get('trackers/search', {'query' => query, 'type' => type})
187
331
  end
188
332
  end
189
333
 
@@ -215,7 +359,7 @@ module PassiveTotal # :nodoc:
215
359
 
216
360
  # returns true if the given string matches a valid classification
217
361
  def classification?(c)
218
- not ['targeted', 'crime', 'multiple', 'benign'].index(c).nil?
362
+ not ["malicious", "non-malicious", "suspicious", "unknown"].index(c).nil?
219
363
  end
220
364
 
221
365
  # returns true is the given object matches true or false
@@ -232,34 +376,56 @@ module PassiveTotal # :nodoc:
232
376
  false
233
377
  end
234
378
 
379
+ def ssl_field?(f)
380
+ return false if f.nil?
381
+ not ["issuerSurname", "subjectOrganizationName", "issuerCountry", "issuerOrganizationUnitName",
382
+ "fingerprint", "subjectOrganizationUnitName", "serialNumber", "subjectEmailAddress", "subjectCountry",
383
+ "issuerGivenName", "subjectCommonName", "issuerCommonName", "issuerStateOrProvinceName", "issuerProvince",
384
+ "subjectStateOrProvinceName", "sha1", "sslVersion", "subjectStreetAddress", "subjectSerialNumber",
385
+ "issuerOrganizationName", "subjectSurname", "subjectLocalityName", "issuerStreetAddress",
386
+ "issuerLocalityName", "subjectGivenName", "subjectProvince", "issuerSerialNumber", "issuerEmailAddress"].index(f).nil?
387
+ end
388
+
389
+ def whois_field?(f)
390
+ return false if f.nil?
391
+ not ["domain", "email", "name", "organization", "address", "phone", "nameserver"].index(f).nil?
392
+ end
393
+
394
+ def tracker_type?(t)
395
+ return false if t.nil?
396
+ not ["YandexMetricaCounterId", "ClickyId", "GoogleAnalyticsAccountNumber", "NewRelicId", "MixpanelId", "GoogleAnalyticsTrackingId"].index(t).nil?
397
+ end
398
+
235
399
  # lowercases and removes a trailing period (if one exists) from a domain name
236
400
  def normalize_domain(domain)
237
401
  return domain.downcase.gsub(/\.$/,'')
238
402
  end
239
403
 
240
404
  # helper function to perform an HTTP GET against the web API
241
- def get(api, query)
242
- params = { 'api_key' => @apikey, 'query' => query }
405
+ def get(api, params={})
406
+ url2json(:GET, "#{@endpoint}#{api}", params)
407
+ end
408
+
409
+ # helper function to perform an HTTP GET against the web API
410
+ def get_params(api, params)
243
411
  url2json(:GET, "#{@endpoint}#{api}", params)
244
412
  end
245
413
 
246
414
  # helper function to perform an HTTP POST against the web API
247
- def post(api, query, set)
248
- params = { 'api_key' => @apikey, 'query' => query, api => set }
415
+ def post(api, params)
249
416
  url2json(:POST, "#{@endpoint}#{api}", params)
250
417
  end
251
418
 
252
- # helper function to perform an HTTP POST against the web API, but sets the parameter to 'tag' instead of the api name
253
- def post_tag(api, query, set)
254
- params = { 'api_key' => @apikey, 'query' => query, 'tag' => set }
255
- url2json(:POST, "#{@endpoint}#{api}", params)
419
+ # helper function to perform an HTTP DELETE against the web API
420
+ def delete(api, params)
421
+ url2json(:DELETE, "#{@endpoint}#{api}", params)
256
422
  end
257
-
423
+
258
424
  # main helper function to perform HTTP interactions with the web API.
259
425
  def url2json(method, url, params)
260
426
  if method == :GET
261
427
  url << "?" + params.map{|k,v| "#{k}=#{v}"}.join("&")
262
- end
428
+ end
263
429
  url = URI.parse url
264
430
  http = Net::HTTP.new(url.host, url.port)
265
431
  http.use_ssl = (url.scheme == 'https')
@@ -268,10 +434,24 @@ module PassiveTotal # :nodoc:
268
434
  request = nil
269
435
  if method == :GET
270
436
  request = Net::HTTP::Get.new(url.request_uri)
271
- else
437
+ elsif method == :POST
272
438
  request = Net::HTTP::Post.new(url.request_uri)
439
+ form_data = params.to_json
440
+ request.content_type = 'application/json'
441
+ request.body = form_data
442
+ elsif method == :DELETE
443
+ request = Net::HTTP::Delete.new(url.request_uri)
444
+ form_data = params.to_json
445
+ request.content_type = 'application/json'
446
+ request.body = form_data
447
+ elsif method == :HEAD
448
+ request = Net::HTTP::Head.new(url.request_uri)
449
+ request.set_form_data(params)
450
+ elsif method == :PUT
451
+ request = Net::HTTP::Put.new(url.request_uri)
273
452
  request.set_form_data(params)
274
453
  end
454
+ request.basic_auth(@username, @apikey)
275
455
  request.add_field("User-Agent", "Ruby/#{RUBY_VERSION} passivetotal rubygem v#{PassiveTotal::VERSION}")
276
456
  t1 = Time.now
277
457
  response = http.request(request)
@@ -280,9 +460,21 @@ module PassiveTotal # :nodoc:
280
460
 
281
461
  obj = Transaction.new(
282
462
  Query.new(method, params['query'], params[method] || params['tag'], url, params),
283
- Response.new(response.body, data['success'], data['request_time'], data['raw_query'], data['error'], data['result_count'], data['results']),
463
+ Response.new(response.body, response.code == '200', data),
284
464
  delta
285
465
  )
466
+
467
+ if data['error']
468
+ message = data['error']['message']
469
+ case message
470
+ when "API key provided does not match any user."
471
+ raise InvalidAPIKeyError.new(obj)
472
+ when "Quota has been exceeded!"
473
+ raise ExceededQuotaError.new(obj)
474
+ else
475
+ raise APIUsageError.new(obj)
476
+ end
477
+ end
286
478
 
287
479
  return obj
288
480
  end
@@ -302,6 +494,12 @@ module PassiveTotal # :nodoc:
302
494
  return true if tag?(item)
303
495
  elsif type == :bool
304
496
  return true if bool?(item)
497
+ elsif type == :ssl_field
498
+ return true if ssl_field?(item)
499
+ elsif type == :whois_field
500
+ return true if whois_field?(item)
501
+ elsif type == :tracker_type
502
+ return true if tracker_type?(item)
305
503
  end
306
504
  end
307
505
  return false
@@ -20,6 +20,7 @@ module PassiveTotal # :nodoc:
20
20
  opts = GetoptLong.new(
21
21
  [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
22
22
  [ '--debug', '-v', GetoptLong::NO_ARGUMENT ],
23
+ [ '--username', '-u', GetoptLong::REQUIRED_ARGUMENT ],
23
24
  [ '--apikey', '-k', GetoptLong::REQUIRED_ARGUMENT ],
24
25
  [ '--metadata', '-m', GetoptLong::REQUIRED_ARGUMENT ],
25
26
  [ '--passive', '-p', GetoptLong::REQUIRED_ARGUMENT ],
@@ -31,6 +32,9 @@ module PassiveTotal # :nodoc:
31
32
  [ '--dynamic', '-d', GetoptLong::REQUIRED_ARGUMENT ],
32
33
  [ '--watching', '-w', GetoptLong::REQUIRED_ARGUMENT ],
33
34
  [ '--sslcertificate', '-l', GetoptLong::REQUIRED_ARGUMENT ],
35
+ [ '--ssl_history', '-H', GetoptLong::REQUIRED_ARGUMENT ],
36
+ [ '--trackers', '-T', GetoptLong::REQUIRED_ARGUMENT ],
37
+ [ '--osint', '-o', GetoptLong::REQUIRED_ARGUMENT ],
34
38
  [ '--set', '-i', GetoptLong::REQUIRED_ARGUMENT ]
35
39
  )
36
40
 
@@ -39,7 +43,8 @@ module PassiveTotal # :nodoc:
39
43
  :query => nil,
40
44
  :set => nil,
41
45
  :debug => false,
42
- :apikey => ENV['PASSIVETOTAL_APIKEY']
46
+ :apikey => ENV['PASSIVETOTAL_APIKEY'],
47
+ :username => ENV['PASSIVETOTAL_USERNAME']
43
48
  }
44
49
 
45
50
  opts.each do |opt, arg|
@@ -48,6 +53,8 @@ module PassiveTotal # :nodoc:
48
53
  options[:method] = :usage
49
54
  when '--debug'
50
55
  options[:debug] = true
56
+ when '--username'
57
+ options[:username] = arg
51
58
  when '--apikey'
52
59
  options[:apikey] = arg
53
60
  when '--metadata'
@@ -80,6 +87,15 @@ module PassiveTotal # :nodoc:
80
87
  when '--sslcertificate'
81
88
  options[:method] = :ssl_certificate
82
89
  options[:query] = arg
90
+ when '--ssl_history'
91
+ options[:method] = :ssl_certificate_history
92
+ options[:query] = arg
93
+ when '--trackers'
94
+ options[:method] = :trackers
95
+ options[:query] = arg
96
+ when '--osint'
97
+ options[:method] = :osint
98
+ options[:query] = arg
83
99
  when '--set'
84
100
  options[:set] = arg.dup
85
101
  else
@@ -100,11 +116,12 @@ module PassiveTotal # :nodoc:
100
116
 
101
117
  if options[:debug]
102
118
  $stderr.puts "PassiveTotal CLI Options"
103
- $stderr.puts " apikey: #{options[:apikey]}"
104
- $stderr.puts " debug: #{options[:debug]}"
105
- $stderr.puts " method: #{options[:method]}"
106
- $stderr.puts " query: #{options[:query]}"
107
- $stderr.puts " set: #{options[:set]}"
119
+ $stderr.puts " username: #{options[:username]}"
120
+ $stderr.puts " apikey: #{options[:apikey]}"
121
+ $stderr.puts " debug: #{options[:debug]}"
122
+ $stderr.puts " method: #{options[:method]}"
123
+ $stderr.puts " query: #{options[:query]}"
124
+ $stderr.puts " set: #{options[:set]}"
108
125
  end
109
126
 
110
127
  return options
@@ -112,9 +129,10 @@ module PassiveTotal # :nodoc:
112
129
 
113
130
  # returns a string containing the usage information
114
131
  def self.usage
115
- help_text = "Usage: #{$0} [-h] [-v] [-k <apikey>] [[-m|-p|-c|-t|-e|-w] <ip or dom>] [[-s|-d] <dom>] [-x <ip>] [-l <ip or hash>] [-i <value>]\n"
132
+ help_text = "Usage: #{$0} [-v] [-u <username>] [-k <apikey>] <action flag> <query> [-i <value>]\n"
116
133
  help_text << "-h Help\n"
117
134
  help_text << "-v Verbose output\n"
135
+ help_text << "-u <username> Sets the Username, defaults to the environment variable PASSIVETOTAL_USERNAME\n"
118
136
  help_text << "-k <apikey> Sets the APIKEY, defaults to the environment variable PASSIVETOTAL_APIKEY\n"
119
137
  help_text << "ACTIONS (You have to select one, last one wins)"
120
138
  help_text << " -m <ip or dom> Queries metadata for given IP or domain\n"
@@ -127,11 +145,14 @@ module PassiveTotal # :nodoc:
127
145
  help_text << " -s <dom> Queries the subdomains for a given domain\n"
128
146
  help_text << " -d <dom> Queries (or sets) if a domain is a dynamic DNS domain\n"
129
147
  help_text << " -x <ip> Queries (or sets) if a given IP is a sinkhole\n"
130
- help_text << " -l <ip or hash> Queries for SSL Certificates/IP addresses associated with a given IP or SHA-1 hash\n"
148
+ help_text << " -l <hash> Queries for SSL certificates/IP addresses associated with a given SHA-1 hash\n"
149
+ help_text << " -H <ip or hash> Queries for SSL certificate history associated with a given IP or SHA-1 hash\n"
150
+ help_text << " -T <ip or dom> Queries for Tracker information associated with a given IP or domain\n"
151
+ help_text << " -o <ip or dom> Queries for OSINT on a given IP or domain\n"
131
152
  help_text << "SETTING VALUES"
132
153
  help_text << " -i <value> Sets the value, used in conjuntion with -c, -t, -e, -w, -d, or -x\n"
133
154
  help_text << " Valid values for -i depend on what it's used with:\n"
134
- help_text << " -c : targeted, crime, multiple, benign\n"
155
+ help_text << " -c : malicious, non-malicious, suspicious, unknown\n"
135
156
  help_text << " -t : <a tag name consisting of characters: [a-zA-Z_]>\n"
136
157
  help_text << " -e, -w, -d, -x: true, false\n"
137
158
  help_text
@@ -141,14 +162,14 @@ module PassiveTotal # :nodoc:
141
162
  def self.run(args)
142
163
  options = parse_command_line(args)
143
164
  return usage() if options[:method] == :usage
144
- pt = PassiveTotal::API.new(options[:apikey])
165
+ pt = PassiveTotal::API.new(options[:username], options[:apikey])
145
166
  if pt.respond_to?(options[:method])
146
167
  if options[:set]
147
168
  data = pt.send(options[:method], options[:query], options[:set])
148
169
  else
149
170
  data = pt.send(options[:method], options[:query])
150
171
  end
151
- data.response.results['response_time'] = data.query.response_time
172
+ data.response.results['response_time'] = data.response_time
152
173
  return JSON.pretty_generate(data.response.results)
153
174
  end
154
175
  return ''
@@ -1,3 +1,3 @@
1
1
  module PassiveTotal
2
- VERSION = "0.2.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: passivetotal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - chrislee35
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-04 00:00:00.000000000 Z
11
+ date: 2016-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json