Skip to content

Commit

Permalink
Add ou:candidate to track candidates for roles within an org. (#3969)
Browse files Browse the repository at this point in the history
  • Loading branch information
invisig0th authored Nov 4, 2024
1 parent 9bc31aa commit 6de8d79
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 9 deletions.
5 changes: 5 additions & 0 deletions changes/64bfe97ecc771ede65ead4295b9efbed.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
desc: Added ``:src:txfiles`` and ``:dst:txfiles`` to ``inet:flow`` to capture transferred files.
prs: []
type: model
...
5 changes: 5 additions & 0 deletions changes/ee9b100466c6b1ba3cd94317254a5013.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
desc: Added ``file:attachment`` to unify file attachment types.
prs: []
type: model
...
5 changes: 5 additions & 0 deletions changes/fe9e8839dd50e3e951583b6facf6f787.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
desc: Added ``ou:candidate`` to track job applications and candidates.
prs: []
type: model
...
11 changes: 8 additions & 3 deletions synapse/datamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,13 +853,17 @@ def addDataModels(self, mods):
for _, mdef in mods:

for formname, forminfo, propdefs in mdef.get('forms', ()):
self.addForm(formname, forminfo, propdefs)
self.addForm(formname, forminfo, propdefs, checks=False)

# now we can load edge definitions...
for _, mdef in mods:
for etype, einfo in mdef.get('edges', ()):
self.addEdge(etype, einfo)

# now we can check the forms display settings...
for form in self.forms.values():
self._checkFormDisplay(form)

def addEdge(self, edgetype, edgeinfo):

n1form, verb, n2form = edgetype
Expand Down Expand Up @@ -915,7 +919,7 @@ def addType(self, typename, basename, typeopts, typeinfo):
self.types[typename] = newtype
self._modeldef['types'].append(newtype.getTypeDef())

def addForm(self, formname, forminfo, propdefs):
def addForm(self, formname, forminfo, propdefs, checks=True):

if not s_grammar.isFormName(formname):
mesg = f'Invalid form name {formname}'
Expand Down Expand Up @@ -949,7 +953,8 @@ def addForm(self, formname, forminfo, propdefs):
for ifname in form.type.info.get('interfaces', ()):
self._addFormIface(form, ifname)

self._checkFormDisplay(form)
if checks:
self._checkFormDisplay(form)

self.formprefixcache.clear()

Expand Down
22 changes: 22 additions & 0 deletions synapse/models/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,16 @@ def getModelDefs(self):
'doc': 'A parent file that fully contains the specified child file.',
}),

('file:attachment', ('guid', {}), {
'display': {
'columns': (
{'type': 'prop', 'opts': {'name': 'name'}},
{'type': 'prop', 'opts': {'name': 'file'}},
{'type': 'prop', 'opts': {'name': 'text'}},
),
},
'doc': 'A file attachment.'}),

('file:archive:entry', ('guid', {}), {
'doc': 'An archive entry representing a file and metadata within a parent archive file.'}),

Expand Down Expand Up @@ -601,6 +611,18 @@ def getModelDefs(self):
}),
)),

('file:attachment', {}, (

('name', ('file:path', {}), {
'doc': 'The name of the attached file.'}),

('text', ('str', {}), {
'doc': 'Any text associated with the file such as alt-text for images.'}),

('file', ('file:bytes', {}), {
'doc': 'The file which was attached.'}),
)),

('file:archive:entry', {}, (

('parent', ('file:bytes', {}), {
Expand Down
14 changes: 10 additions & 4 deletions synapse/models/inet.py
Original file line number Diff line number Diff line change
Expand Up @@ -2015,8 +2015,11 @@ def getModelDefs(self):
'doc': 'The guid of the destination process.'
}),
('dst:exe', ('file:bytes', {}), {
'doc': 'The file (executable) that received the connection.'
}),
'doc': 'The file (executable) that received the connection.'}),

('dst:txfiles', ('array', {'type': 'file:attachment', 'sorted': True, 'uniq': True}), {
'doc': 'An array of files sent by the destination host.'}),

('dst:txcount', ('int', {}), {
'doc': 'The number of packets sent by the destination host.'
}),
Expand Down Expand Up @@ -2049,8 +2052,11 @@ def getModelDefs(self):
'doc': 'The guid of the source process.'
}),
('src:exe', ('file:bytes', {}), {
'doc': 'The file (executable) that created the connection.'
}),
'doc': 'The file (executable) that created the connection.'}),

('src:txfiles', ('array', {'type': 'file:attachment', 'sorted': True, 'uniq': True}), {
'doc': 'An array of files sent by the source host.'}),

('src:txcount', ('int', {}), {
'doc': 'The number of packets sent by the source host.'
}),
Expand Down
58 changes: 56 additions & 2 deletions synapse/models/orgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,24 @@ def getModelDefs(self):
'doc': 'Vital statistics about an org for a given time period.',
}),
('ou:opening', ('guid', {}), {
'doc': 'A job/work opening within an org.',
}),
'doc': 'A job/work opening within an org.'}),

('ou:candidate:method:taxonomy', ('taxonomy', {}), {
'interfaces': ('meta:taxonomy',),
'doc': 'A taxonomy of methods by which a candidate came under consideration.'}),

('ou:candidate', ('guid', {}), {
'doc': 'A candidate being considered for a role within an organization.',
'display': {
'columns': (
{'type': 'prop', 'opts': {'name': 'contact::name'}},
{'type': 'prop', 'opts': {'name': 'contact::email'}},
{'type': 'prop', 'opts': {'name': 'submitted'}},
{'type': 'prop', 'opts': {'name': 'org::name'}},
{'type': 'prop', 'opts': {'name': 'opening::jobtitle'}},
),
}}),

('ou:jobtype', ('taxonomy', {}), {
'ex': 'it.dev.python',
'doc': 'A taxonomy of job types.',
Expand Down Expand Up @@ -354,6 +370,44 @@ def getModelDefs(self):
}),
# TODO a way to encode/normalize requirements.
)),
('ou:candidate:method:taxonomy', {}, ()),
('ou:candidate', {}, (

('org', ('ou:org', {}), {
'doc': 'The organization considering the candidate.'}),

('contact', ('ps:contact', {}), {
'doc': 'The contact information of the candidate.'}),

('method', ('ou:candidate:method:taxonomy', {}), {
'doc': 'The method by which the candidate came under consideration.'}),

('submitted', ('time', {}), {
'doc': 'The time the candidate was submitted for consideration.'}),

('intro', ('str', {'strip': True}), {
'doc': 'An introduction or cover letter text submitted by the candidate.'}),

('resume', ('file:bytes', {}), {
'doc': "The candidate's resume or CV."}),

('opening', ('ou:opening', {}), {
'doc': 'The opening that the candidate is being considered for.'}),

('agent', ('ps:contact', {}), {
'doc': 'The contact information of an agent who advocates for the candidate.'}),

('recruiter', ('ps:contact', {}), {
'doc': 'The contact information of a recruiter who works on behalf of the organization.'}),

('attachments', ('array', {'type': 'file:attachment', 'sorted': True, 'uniq': True}), {
'doc': 'An array of additional files submitted by the candidate.'}),

# TODO: doc:questionare / responses
# TODO: :skills=[<ps:skill>]? vs :contact -> ps:proficiency?
# TODO: proj:task to track evaluation of the candidate?

)),
('ou:vitals', {}, (

('asof', ('time', {}), {
Expand Down
19 changes: 19 additions & 0 deletions synapse/tests/test_model_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,3 +638,22 @@ async def test_model_file_lnk(self):
self.eq(node.get('iconindex'), 1)

self.len(1, await core.nodes('file:mime:lnk -> it:hostname'))

async def test_model_file_attachment(self):

async with self.getTestCore() as core:

nodes = await core.nodes('''
[ file:attachment=*
:name=Foo/Bar.exe
:text="foo bar"
:file=*
]
''')
self.len(1, nodes)
self.nn(nodes[0].get('file'))
self.eq('foo bar', nodes[0].get('text'))
self.eq('foo/bar.exe', nodes[0].get('name'))

self.len(1, await core.nodes('file:attachment -> file:bytes'))
self.len(1, await core.nodes('file:attachment -> file:path'))
4 changes: 4 additions & 0 deletions synapse/tests/test_model_inet.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ async def test_flow(self):
:src:rdp:hostname=SYNCODER
:src:rdp:keyboard:layout=AZERTY
:raw=((10), (20))
:src:txfiles={[ file:attachment=* :name=foo.exe ]}
:dst:txfiles={[ file:attachment=* :name=bar.exe ]}
)]'''
nodes = await core.nodes(q, opts={'vars': {'valu': valu, 'p': props}})
self.len(1, nodes)
Expand Down Expand Up @@ -501,6 +503,8 @@ async def test_flow(self):
self.len(2, await core.nodes('inet:flow -> crypto:x509:cert'))
self.len(1, await core.nodes('inet:flow :src:ssh:key -> crypto:key'))
self.len(1, await core.nodes('inet:flow :dst:ssh:key -> crypto:key'))
self.len(1, await core.nodes('inet:flow :src:txfiles -> file:attachment +:name=foo.exe'))
self.len(1, await core.nodes('inet:flow :dst:txfiles -> file:attachment +:name=bar.exe'))

async def test_fqdn(self):
formname = 'inet:fqdn'
Expand Down
26 changes: 26 additions & 0 deletions synapse/tests/test_model_orgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,32 @@ async def test_ou_simple(self):
self.len(1, await core.nodes('ou:enacted :ext:creator -> ps:contact +:name=root'))
self.len(1, await core.nodes('ou:enacted :ext:assignee -> ps:contact +:name=visi'))

nodes = await core.nodes('''
[ ou:candidate=*
:org={ ou:org:name=vertex | limit 1 }
:contact={ ps:contact:name=visi | limit 1 }
:intro=" Hi there!"
:submitted=20241104
:method=referral.employee
:resume=*
:opening=*
:agent={[ ps:contact=* :name=agent ]}
:recruiter={[ ps:contact=* :name=recruiter ]}
:attachments={[ file:attachment=* :name=questions.pdf ]}
]
''')
self.len(1, nodes)
self.eq('Hi there!', nodes[0].get('intro'))
self.eq(1730678400000, nodes[0].get('submitted'))
self.eq('referral.employee.', nodes[0].get('method'))
self.len(1, await core.nodes('ou:candidate :org -> ou:org +:name=vertex'))
self.len(1, await core.nodes('ou:candidate :agent -> ps:contact +:name=agent'))
self.len(1, await core.nodes('ou:candidate :contact -> ps:contact +:name=visi'))
self.len(1, await core.nodes('ou:candidate :recruiter -> ps:contact +:name=recruiter'))

self.len(1, await core.nodes('ou:candidate :method -> ou:candidate:method:taxonomy'))
self.len(1, await core.nodes('ou:candidate :attachments -> file:attachment'))

async def test_ou_code_prefixes(self):
guid0 = s_common.guid()
guid1 = s_common.guid()
Expand Down

0 comments on commit 6de8d79

Please sign in to comment.