diff --git a/internal/parser/elf/meta.go b/internal/parser/elf/meta.go
new file mode 100644
index 0000000..8c37314
--- /dev/null
+++ b/internal/parser/elf/meta.go
@@ -0,0 +1,27 @@
+// Package elf parses output of the elf static analysis tool to
+// assign scores based on detected code issues.
+
+package elf
+
+import "github.com/joint-online-judge/JOJ3/internal/stage"
+
+var name = "elf"
+
+type Match struct {
+	Keywords []string
+	Score    int
+}
+
+type Conf struct {
+	Score             int
+	Matches           []Match
+	Stdout            string `default:"stdout"`
+	Stderr            string `default:"stderr"`
+	ForceQuitOnDeduct bool   `default:"false"`
+}
+
+type Elf struct{}
+
+func init() {
+	stage.RegisterParser(name, &Elf{})
+}
diff --git a/internal/parser/elf/model.go b/internal/parser/elf/model.go
new file mode 100644
index 0000000..f998543
--- /dev/null
+++ b/internal/parser/elf/model.go
@@ -0,0 +1,60 @@
+package elf
+
+import (
+	"fmt"
+	"strings"
+)
+
+type Toplevel struct {
+	Title   string   `json:"title"`
+	Modules []Module `json:"modules"`
+}
+
+type Module struct {
+	Entries   []Entry `json:"entries"`
+	DebugInfo string  `json:"debug_info"`
+}
+
+type Entry []any
+
+type Report struct {
+	File  string `json:"file"`
+	Name  string `json:"name"`
+	Cases []Case `json:"cases" mapstructure:"cases"`
+}
+
+type Case struct {
+	Binders        Binders  `mapstructure:"binders"`
+	Context        string   `mapstructure:"context"`
+	Depths         int      `mapstructure:"depths"`
+	Code           string   `mapstructure:"code"`
+	Plain          int      `mapstructure:"plain"`
+	Weighed        float64  `mapstructure:"weighed"`
+	Detail         string   `mapstructure:"detail"`
+	SimilarityRate float64  `mapstructure:"similarity_rate"`
+	Sources        []Source `mapstructure:"srcs"`
+}
+
+type Binder struct {
+	Binder string `json:"binder"`
+	Pos    string `json:"pos"`
+}
+
+func (b Binder) String() string {
+	return fmt.Sprintf("In the definition of %s (at %s)", b.Binder, b.Pos)
+}
+
+type Binders []Binder
+
+func (bs Binders) String() string {
+	s := make([]string, 0, len(bs))
+	for _, b := range bs {
+		s = append(s, b.String())
+	}
+	return strings.Join(s, "; ")
+}
+
+type Source struct {
+	Context string `json:"context"`
+	Code    string `json:"code"`
+}
diff --git a/internal/parser/elf/parser.go b/internal/parser/elf/parser.go
new file mode 100644
index 0000000..2d2eaea
--- /dev/null
+++ b/internal/parser/elf/parser.go
@@ -0,0 +1,124 @@
+package elf
+
+import (
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"strings"
+
+	"github.com/joint-online-judge/JOJ3/internal/stage"
+	"github.com/mitchellh/mapstructure"
+)
+
+func (p *Elf) parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
+	stdout := executorResult.Files[conf.Stdout]
+	// stderr := executorResult.Files[conf.Stderr]
+	var topLevel Toplevel
+	err := json.Unmarshal([]byte(stdout), &topLevel)
+	if err != nil {
+		return stage.ParserResult{
+			Score: 0,
+			Comment: fmt.Sprintf(
+				"Unexpected parser error: %s.",
+				err,
+			),
+		}
+	}
+	score := conf.Score
+	comment := ""
+	for _, module := range topLevel.Modules {
+		for _, entry := range module.Entries {
+			kind := entry[0].(string)
+			report := Report{}
+			err := mapstructure.Decode(entry[1], &report)
+			if err != nil {
+				slog.Error("elf parse", "mapstructure decode err", err)
+			}
+			comment += fmt.Sprintf("### [%s] %s\n", report.File, report.Name)
+			for _, caseObj := range report.Cases {
+				for _, match := range conf.Matches {
+					for _, keyword := range match.Keywords {
+						if strings.Contains(kind, keyword) {
+							score += -match.Score
+						}
+					}
+				}
+				switch kind {
+				case "ParenDep":
+					// "<binders>:\n<context> below reaches a parentheses depths of <depths>:\n<code>"
+					comment += fmt.Sprintf(
+						"%s:\n%s below reaches a parentheses depths of %d:\n"+
+							"```%s\n```\n",
+						caseObj.Binders,
+						caseObj.Context,
+						caseObj.Depths,
+						caseObj.Code,
+					)
+				case "CodeLen":
+					// "<binders>:\n<context> below excceeds a code length upper bound with <plain> (weighed: <weighed>):\n<code>"
+					comment += fmt.Sprintf(
+						"%s:\n%s below excceeds a code length "+
+							"upper bound with %d (weighed: %f):\n"+
+							"```%s\n```\n",
+						caseObj.Binders,
+						caseObj.Context,
+						caseObj.Plain,
+						caseObj.Weighed,
+						caseObj.Code,
+					)
+				case "OverArity":
+					// "<binders>:\n<context> below hits <detail>:\n<code>"
+					comment += fmt.Sprintf(
+						"%s:\n%s below hits %s:\n```%s\n```\n",
+						caseObj.Binders,
+						caseObj.Context,
+						caseObj.Detail,
+						caseObj.Code,
+					)
+				case "CodeDup":
+					if len(caseObj.Sources) != 2 {
+						slog.Error("elf parse", "code dup sources length", len(caseObj.Sources))
+					}
+					context0 := caseObj.Sources[0].Context
+					code0 := caseObj.Sources[0].Code
+					context1 := caseObj.Sources[1].Context
+					code1 := caseObj.Sources[1].Code
+					// "The code below has a similarity rate of <similarity_rate>:\n- <context1>:\n\t<code1>\n- <context2>:\n\t<code2>"
+					comment += fmt.Sprintf(
+						"The code below has a similarity rate of %f:\n"+
+							"- %s:\n```%s\n```\n"+
+							"- %s:\n```%s\n```\n",
+						caseObj.SimilarityRate,
+						context0,
+						code0,
+						context1,
+						code1,
+					)
+				}
+			}
+		}
+	}
+	return stage.ParserResult{
+		Score:   score,
+		Comment: comment,
+	}
+}
+
+func (p *Elf) Run(results []stage.ExecutorResult, confAny any) (
+	[]stage.ParserResult, bool, error,
+) {
+	conf, err := stage.DecodeConf[Conf](confAny)
+	if err != nil {
+		return nil, true, err
+	}
+	res := make([]stage.ParserResult, 0, len(results))
+	forceQuit := false
+	for _, result := range results {
+		parseRes := p.parse(result, *conf)
+		if conf.ForceQuitOnDeduct && parseRes.Score < conf.Score {
+			forceQuit = true
+		}
+		res = append(res, parseRes)
+	}
+	return res, forceQuit, nil
+}