passivetotal 0.2.0 → 1.0.0

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.
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