Skip to content

Commit 0f9abd6

Browse files
committed
initial version of the Aviato Company Funding Rounds handler
1 parent cb86b8f commit 0f9abd6

File tree

3 files changed

+343
-15
lines changed

3 files changed

+343
-15
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Copyright © by Jeff Foley 2017-2025. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package aviato
6+
7+
import (
8+
"context"
9+
"encoding/json"
10+
"errors"
11+
"fmt"
12+
"log/slog"
13+
"net/url"
14+
"strings"
15+
"time"
16+
17+
"github.com/owasp-amass/amass/v4/engine/plugins/support"
18+
et "github.com/owasp-amass/amass/v4/engine/types"
19+
"github.com/owasp-amass/amass/v4/utils/net/http"
20+
dbt "github.com/owasp-amass/asset-db/types"
21+
oam "github.com/owasp-amass/open-asset-model"
22+
"github.com/owasp-amass/open-asset-model/general"
23+
"github.com/owasp-amass/open-asset-model/org"
24+
"github.com/owasp-amass/open-asset-model/people"
25+
)
26+
27+
func (cr *companyRounds) check(e *et.Event) error {
28+
oamid, ok := e.Entity.Asset.(*general.Identifier)
29+
if !ok {
30+
return errors.New("failed to extract the Identifier asset")
31+
} else if oamid.Type != AviatoCompanyID {
32+
return nil
33+
}
34+
35+
ds := e.Session.Config().GetDataSourceConfig(cr.plugin.name)
36+
if ds == nil || len(ds.Creds) == 0 {
37+
return nil
38+
}
39+
40+
var keys []string
41+
for _, cr := range ds.Creds {
42+
if cr != nil && cr.Apikey != "" {
43+
keys = append(keys, cr.Apikey)
44+
}
45+
}
46+
if len(keys) == 0 {
47+
return nil
48+
}
49+
50+
since, err := support.TTLStartTime(e.Session.Config(),
51+
string(oam.Identifier), string(oam.Organization), cr.plugin.name)
52+
if err != nil {
53+
return err
54+
}
55+
56+
var orgent *dbt.Entity
57+
var employents []*dbt.Entity
58+
src := &et.Source{
59+
Name: cr.name,
60+
Confidence: cr.plugin.source.Confidence,
61+
}
62+
if support.AssetMonitoredWithinTTL(e.Session, e.Entity, src, since) {
63+
orgent, employents = cr.lookup(e, e.Entity, since)
64+
} else {
65+
orgent, employents = cr.query(e, e.Entity, keys)
66+
support.MarkAssetMonitored(e.Session, e.Entity, src)
67+
}
68+
69+
if orgent != nil && len(employents) > 0 {
70+
cr.process(e, orgent, employents)
71+
}
72+
return nil
73+
}
74+
75+
func (cr *companyRounds) lookup(e *et.Event, ident *dbt.Entity, since time.Time) (*dbt.Entity, []*dbt.Entity) {
76+
var orgent *dbt.Entity
77+
78+
if edges, err := e.Session.Cache().IncomingEdges(ident, since, "id"); err == nil {
79+
for _, edge := range edges {
80+
if tags, err := e.Session.Cache().GetEdgeTags(edge, since, cr.plugin.source.Name); err != nil || len(tags) == 0 {
81+
continue
82+
}
83+
if a, err := e.Session.Cache().FindEntityById(edge.FromEntity.ID); err == nil && a != nil {
84+
if _, ok := a.Asset.(*org.Organization); ok {
85+
orgent = a
86+
break
87+
}
88+
}
89+
}
90+
}
91+
92+
var fundents []*dbt.Entity
93+
if orgent == nil {
94+
return nil, fundents
95+
}
96+
97+
if edges, err := e.Session.Cache().OutgoingEdges(orgent, since, "member"); err == nil {
98+
for _, edge := range edges {
99+
if tags, err := e.Session.Cache().GetEdgeTags(edge, since, cr.plugin.source.Name); err != nil || len(tags) == 0 {
100+
continue
101+
}
102+
if a, err := e.Session.Cache().FindEntityById(edge.ToEntity.ID); err == nil && a != nil {
103+
if _, ok := a.Asset.(*people.Person); ok {
104+
fundents = append(fundents, a)
105+
}
106+
}
107+
}
108+
}
109+
110+
return orgent, fundents
111+
}
112+
113+
func (cr *companyRounds) query(e *et.Event, ident *dbt.Entity, apikey []string) (*dbt.Entity, []*dbt.Entity) {
114+
oamid := e.Entity.Asset.(*general.Identifier)
115+
116+
orgent := cr.getAssociatedOrg(e, ident)
117+
if orgent == nil {
118+
msg := fmt.Sprintf("failed to find the Organization asset for %s", oamid.UniqueID)
119+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
120+
return nil, []*dbt.Entity{}
121+
}
122+
123+
page := 0
124+
total := 1
125+
perPage := 100
126+
var fundents []*dbt.Entity
127+
loop:
128+
for _, key := range apikey {
129+
for ; page < total; page++ {
130+
headers := http.Header{"Content-Type": []string{"application/json"}}
131+
headers["Authorization"] = []string{"Bearer " + key}
132+
133+
_ = cr.plugin.rlimit.Wait(context.TODO())
134+
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
135+
defer cancel()
136+
137+
u := fmt.Sprintf("https://data.api.aviato.co/company/%s/funding-rounds?perPage=%d&page=%d", url.QueryEscape(oamid.ID), perPage, page)
138+
resp, err := http.RequestWebPage(ctx, &http.Request{URL: u, Header: headers})
139+
if err != nil {
140+
msg := fmt.Sprintf("failed to obtain the funding rounds for %s: %s", oamid.ID, err)
141+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
142+
continue
143+
} else if resp.StatusCode != 200 {
144+
msg := fmt.Sprintf("failed to obtain the funding rounds for %s: %s", oamid.ID, resp.Status)
145+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
146+
continue
147+
} else if resp.Body == "" {
148+
msg := fmt.Sprintf("failed to obtain the funding rounds for %s: empty body", oamid.ID)
149+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
150+
continue
151+
} else if strings.Contains(resp.Body, "error") {
152+
msg := fmt.Sprintf("failed to obtain the funding rounds for %s: %s", oamid.ID, resp.Body)
153+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
154+
continue
155+
}
156+
157+
var result companyRoundsResult
158+
if err := json.Unmarshal([]byte(resp.Body), &result); err != nil {
159+
msg := fmt.Sprintf("failed to unmarshal the funding rounds for %s: %s", oamid.ID, err)
160+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
161+
break loop
162+
} else if len(result.FundingRounds) == 0 {
163+
break loop
164+
}
165+
166+
if len(result.FundingRounds) > 0 {
167+
if ents := cr.store(e, ident, orgent, &result); len(ents) > 0 {
168+
fundents = append(fundents, ents...)
169+
}
170+
}
171+
172+
total = result.Pages
173+
}
174+
175+
if page >= total {
176+
break
177+
}
178+
}
179+
180+
if len(fundents) == 0 {
181+
return nil, []*dbt.Entity{}
182+
}
183+
return orgent, fundents
184+
}
185+
186+
func (cr *companyRounds) getAssociatedOrg(e *et.Event, ident *dbt.Entity) *dbt.Entity {
187+
var orgent *dbt.Entity
188+
189+
if edges, err := e.Session.Cache().IncomingEdges(ident, time.Time{}, "id"); err == nil {
190+
for _, edge := range edges {
191+
if a, err := e.Session.Cache().FindEntityById(edge.FromEntity.ID); err == nil && a != nil {
192+
if _, ok := a.Asset.(*org.Organization); ok {
193+
orgent = a
194+
break
195+
}
196+
}
197+
}
198+
}
199+
200+
return orgent
201+
}
202+
203+
func (cr *companyRounds) store(e *et.Event, ident, orgent *dbt.Entity, funds *companyRoundsResult) []*dbt.Entity {
204+
var fundents []*dbt.Entity
205+
206+
/*for _, round := range funds.FundingRounds {
207+
p := support.FullNameToPerson(emp.Person.FullName)
208+
if p == nil {
209+
continue
210+
}
211+
212+
personent, err := e.Session.Cache().CreateAsset(p)
213+
if err != nil {
214+
msg := fmt.Sprintf("failed to create the Person asset for %s: %s", p.FullName, err)
215+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
216+
continue
217+
}
218+
219+
_, err = e.Session.Cache().CreateEntityProperty(personent, &general.SourceProperty{
220+
Source: cr.name,
221+
Confidence: cr.plugin.source.Confidence,
222+
})
223+
if err != nil {
224+
msg := fmt.Sprintf("failed to create the Person asset source property for %s: %s", p.FullName, err)
225+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
226+
continue
227+
}
228+
229+
if err := cr.plugin.createRelation(e.Session, orgent,
230+
general.SimpleRelation{Name: "member"}, personent, cr.plugin.source.Confidence); err == nil {
231+
employents = append(employents, personent)
232+
} else {
233+
msg := fmt.Sprintf("failed to create the member relation for %s: %s", p.FullName, err)
234+
e.Session.Log().Error(msg, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
235+
}
236+
}*/
237+
238+
return fundents
239+
}
240+
241+
func (cr *companyRounds) process(e *et.Event, orgent *dbt.Entity, fundents []*dbt.Entity) {
242+
/*
243+
for _, fund := range fundents {
244+
p := employee.Asset.(*people.Person)
245+
246+
_ = e.Dispatcher.DispatchEvent(&et.Event{
247+
Name: fmt.Sprintf("%s:%s", p.FullName, p.ID),
248+
Entity: employee,
249+
Session: e.Session,
250+
})
251+
252+
o := orgent.Asset.(*org.Organization)
253+
e.Session.Log().Info("relationship discovered", "from", o.Name, "relation", "member",
254+
"to", p.FullName, slog.Group("plugin", "name", cr.plugin.name, "handler", cr.name))
255+
}
256+
*/
257+
}

engine/plugins/api/aviato/plugin.go

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,38 @@ func (a *aviato) Name() string {
3535
func (a *aviato) Start(r et.Registry) error {
3636
a.log = r.Log().WithGroup("plugin").With("name", a.name)
3737

38-
a.companySearch = &companySearch{
39-
name: a.name + "-Company-Search-Handler",
38+
a.companyEnrich = &companyEnrich{
39+
name: a.name + "-Company-Enrich-Handler",
4040
plugin: a,
4141
}
4242

4343
if err := r.RegisterHandler(&et.Handler{
4444
Plugin: a,
45-
Name: a.companySearch.name,
46-
Priority: 6,
47-
Transforms: []string{string(oam.Identifier)},
48-
EventType: oam.Organization,
49-
Callback: a.companySearch.check,
45+
Name: a.companyEnrich.name,
46+
Priority: 7,
47+
Transforms: []string{string(oam.Organization)},
48+
EventType: oam.Identifier,
49+
Callback: a.companyEnrich.check,
50+
}); err != nil {
51+
return err
52+
}
53+
54+
a.companyRounds = &companyRounds{
55+
name: a.name + "-Company-Rounds-Handler",
56+
plugin: a,
57+
}
58+
59+
if err := r.RegisterHandler(&et.Handler{
60+
Plugin: a,
61+
Name: a.companyRounds.name,
62+
Priority: 7,
63+
Transforms: []string{
64+
string(oam.Organization),
65+
string(oam.Account),
66+
string(oam.FundsTransfer),
67+
},
68+
EventType: oam.Identifier,
69+
Callback: a.companyRounds.check,
5070
}); err != nil {
5171
return err
5272
}
@@ -67,18 +87,18 @@ func (a *aviato) Start(r et.Registry) error {
6787
return err
6888
}
6989

70-
a.companyEnrich = &companyEnrich{
71-
name: a.name + "-Company-Enrich-Handler",
90+
a.companySearch = &companySearch{
91+
name: a.name + "-Company-Search-Handler",
7292
plugin: a,
7393
}
7494

7595
if err := r.RegisterHandler(&et.Handler{
7696
Plugin: a,
77-
Name: a.companyEnrich.name,
78-
Priority: 7,
79-
Transforms: []string{string(oam.Organization)},
80-
EventType: oam.Identifier,
81-
Callback: a.companyEnrich.check,
97+
Name: a.companySearch.name,
98+
Priority: 6,
99+
Transforms: []string{string(oam.Identifier)},
100+
EventType: oam.Organization,
101+
Callback: a.companySearch.check,
82102
}); err != nil {
83103
return err
84104
}

engine/plugins/api/aviato/types.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ type aviato struct {
2020
name string
2121
log *slog.Logger
2222
rlimit *rate.Limiter
23-
companySearch *companySearch
2423
companyEnrich *companyEnrich
24+
companyRounds *companyRounds
2525
employees *employees
26+
companySearch *companySearch
2627
source *et.Source
2728
}
2829

@@ -58,6 +59,56 @@ type companyEnrich struct {
5859
plugin *aviato
5960
}
6061

62+
type companyRounds struct {
63+
name string
64+
plugin *aviato
65+
}
66+
67+
type companyRoundsResult struct {
68+
FundingRounds []struct {
69+
ID int `json:"id"`
70+
AnnouncedOn string `json:"announcedOn"`
71+
MoneyRaised int `json:"moneyRaised"`
72+
Name string `json:"name"`
73+
Stage string `json:"stage"`
74+
LeadPersonInvestors []struct {
75+
PersonID string `json:"id"`
76+
FullName string `json:"fullName"`
77+
EntityType string `json:"entityType"`
78+
Location string `json:"location"`
79+
URLs URLResult `json:"URLs"`
80+
} `json:"leadPersonInvestorList"`
81+
PersonInvestors []struct {
82+
PersonID string `json:"id"`
83+
FullName string `json:"fullName"`
84+
EntityType string `json:"entityType"`
85+
Location string `json:"location"`
86+
URLs URLResult `json:"URLs"`
87+
} `json:"personInvestorList"`
88+
LeadCompanyInvestors []struct {
89+
CompanyID string `json:"id"`
90+
Name string `json:"name"`
91+
Country string `json:"country"`
92+
Region string `json:"region"`
93+
Locality string `json:"locality"`
94+
URLs URLResult `json:"URLs"`
95+
IndustryList []string `json:"industryList"`
96+
} `json:"leadCompanyInvestorList"`
97+
CompanyInvestors []struct {
98+
CompanyID string `json:"id"`
99+
Name string `json:"name"`
100+
Country string `json:"country"`
101+
Region string `json:"region"`
102+
Locality string `json:"locality"`
103+
URLs URLResult `json:"URLs"`
104+
IndustryList []string `json:"industryList"`
105+
} `json:"companyInvestorList"`
106+
CompanyID string `json:"companyID"`
107+
}
108+
Pages int `json:"pages"`
109+
TotalResults int `json:"totalResults"`
110+
}
111+
61112
type companySearch struct {
62113
name string
63114
plugin *aviato

0 commit comments

Comments
 (0)